@quiltt/core 3.6.12 → 3.6.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,188 +0,0 @@
1
- // @ts-nocheck
2
- import adapters from './adapters'
3
- import ConnectionMonitor from './connection_monitor'
4
- import INTERNAL from './internal'
5
- import logger from './logger'
6
-
7
- // Encapsulate the cable connection held by the consumer. This is an internal class not intended for direct user manipulation.
8
-
9
- const { message_types, protocols } = INTERNAL
10
- const supportedProtocols = protocols.slice(0, protocols.length - 1)
11
-
12
- const indexOf = [].indexOf
13
-
14
- class Connection {
15
- constructor(consumer) {
16
- this.open = this.open.bind(this)
17
- this.consumer = consumer
18
- this.subscriptions = this.consumer.subscriptions
19
- this.monitor = new ConnectionMonitor(this)
20
- this.disconnected = true
21
- }
22
-
23
- send(data) {
24
- if (this.isOpen()) {
25
- this.webSocket.send(JSON.stringify(data))
26
- return true
27
- }
28
- return false
29
- }
30
-
31
- open() {
32
- if (this.isActive()) {
33
- logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`)
34
- return false
35
- }
36
- const socketProtocols = [...protocols, ...(this.consumer.subprotocols || [])]
37
- logger.log(
38
- `Opening WebSocket, current state is ${this.getState()}, subprotocols: ${socketProtocols}`
39
- )
40
- if (this.webSocket) {
41
- this.uninstallEventHandlers()
42
- }
43
- this.webSocket = new adapters.WebSocket(this.consumer.url, socketProtocols)
44
- this.installEventHandlers()
45
- this.monitor.start()
46
- return true
47
- }
48
-
49
- close({ allowReconnect } = { allowReconnect: true }) {
50
- if (!allowReconnect) {
51
- this.monitor.stop()
52
- }
53
- // Avoid closing websockets in a "connecting" state due to Safari 15.1+ bug. See: https://github.com/rails/rails/issues/43835#issuecomment-1002288478
54
- if (this.isOpen()) {
55
- return this.webSocket.close()
56
- }
57
- }
58
-
59
- reopen() {
60
- logger.log(`Reopening WebSocket, current state is ${this.getState()}`)
61
- if (this.isActive()) {
62
- try {
63
- return this.close()
64
- } catch (error) {
65
- logger.log('Failed to reopen WebSocket', error)
66
- } finally {
67
- logger.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`)
68
- setTimeout(this.open, this.constructor.reopenDelay)
69
- }
70
- } else {
71
- return this.open()
72
- }
73
- }
74
-
75
- getProtocol() {
76
- if (this.webSocket) {
77
- return this.webSocket.protocol
78
- }
79
- }
80
-
81
- isOpen() {
82
- return this.isState('open')
83
- }
84
-
85
- isActive() {
86
- return this.isState('open', 'connecting')
87
- }
88
-
89
- triedToReconnect() {
90
- return this.monitor.reconnectAttempts > 0
91
- }
92
-
93
- // Private
94
-
95
- isProtocolSupported() {
96
- return indexOf.call(supportedProtocols, this.getProtocol()) >= 0
97
- }
98
-
99
- isState(...states) {
100
- return indexOf.call(states, this.getState()) >= 0
101
- }
102
-
103
- getState() {
104
- if (this.webSocket) {
105
- for (const state in adapters.WebSocket) {
106
- if (adapters.WebSocket[state] === this.webSocket.readyState) {
107
- return state.toLowerCase()
108
- }
109
- }
110
- }
111
- return null
112
- }
113
-
114
- installEventHandlers() {
115
- for (const eventName in this.events) {
116
- const handler = this.events[eventName].bind(this)
117
- this.webSocket[`on${eventName}`] = handler
118
- }
119
- }
120
-
121
- uninstallEventHandlers() {
122
- for (const eventName in this.events) {
123
- this.webSocket[`on${eventName}`] = () => {}
124
- }
125
- }
126
- }
127
-
128
- Connection.reopenDelay = 500
129
-
130
- Connection.prototype.events = {
131
- message(event) {
132
- if (!this.isProtocolSupported()) {
133
- return
134
- }
135
- const { identifier, message, reason, reconnect, type } = JSON.parse(event.data)
136
- switch (type) {
137
- case message_types.welcome:
138
- if (this.triedToReconnect()) {
139
- this.reconnectAttempted = true
140
- }
141
- this.monitor.recordConnect()
142
- return this.subscriptions.reload()
143
- case message_types.disconnect:
144
- logger.log(`Disconnecting. Reason: ${reason}`)
145
- return this.close({ allowReconnect: reconnect })
146
- case message_types.ping:
147
- return this.monitor.recordPing()
148
- case message_types.confirmation:
149
- this.subscriptions.confirmSubscription(identifier)
150
- if (this.reconnectAttempted) {
151
- this.reconnectAttempted = false
152
- return this.subscriptions.notify(identifier, 'connected', { reconnected: true })
153
- }
154
- return this.subscriptions.notify(identifier, 'connected', { reconnected: false })
155
- case message_types.rejection:
156
- return this.subscriptions.reject(identifier)
157
- default:
158
- return this.subscriptions.notify(identifier, 'received', message)
159
- }
160
- },
161
-
162
- open() {
163
- logger.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`)
164
- this.disconnected = false
165
- if (!this.isProtocolSupported()) {
166
- logger.log('Protocol is unsupported. Stopping monitor and disconnecting.')
167
- return this.close({ allowReconnect: false })
168
- }
169
- },
170
-
171
- close(_event) {
172
- logger.log('WebSocket onclose event')
173
- if (this.disconnected) {
174
- return
175
- }
176
- this.disconnected = true
177
- this.monitor.recordDisconnect()
178
- return this.subscriptions.notifyAll('disconnected', {
179
- willAttemptReconnect: this.monitor.isRunning(),
180
- })
181
- },
182
-
183
- error() {
184
- logger.log('WebSocket onerror event')
185
- },
186
- }
187
-
188
- export default Connection
@@ -1,141 +0,0 @@
1
- // @ts-nocheck
2
- import logger from './logger'
3
-
4
- // Responsible for ensuring the cable connection is in good health by validating the heartbeat pings sent from the server, and attempting
5
- // revival reconnections if things go astray. Internal class, not intended for direct user manipulation.
6
-
7
- const now = () => new Date().getTime()
8
-
9
- const secondsSince = (time) => (now() - time) / 1000
10
-
11
- class ConnectionMonitor {
12
- constructor(connection) {
13
- this.visibilityDidChange = this.visibilityDidChange.bind(this)
14
- this.connection = connection
15
- this.reconnectAttempts = 0
16
- }
17
-
18
- start() {
19
- if (!this.isRunning()) {
20
- this.startedAt = now()
21
- // biome-ignore lint/performance/noDelete: <explanation>
22
- delete this.stoppedAt
23
- this.startPolling()
24
- addEventListener('visibilitychange', this.visibilityDidChange)
25
- logger.log(
26
- `ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`
27
- )
28
- }
29
- }
30
-
31
- stop() {
32
- if (this.isRunning()) {
33
- this.stoppedAt = now()
34
- this.stopPolling()
35
- removeEventListener('visibilitychange', this.visibilityDidChange)
36
- logger.log('ConnectionMonitor stopped')
37
- }
38
- }
39
-
40
- isRunning() {
41
- return this.startedAt && !this.stoppedAt
42
- }
43
-
44
- recordPing() {
45
- this.pingedAt = now()
46
- }
47
-
48
- recordConnect() {
49
- this.reconnectAttempts = 0
50
- this.recordPing()
51
- // biome-ignore lint/performance/noDelete: <explanation>
52
- delete this.disconnectedAt
53
- logger.log('ConnectionMonitor recorded connect')
54
- }
55
-
56
- recordDisconnect() {
57
- this.disconnectedAt = now()
58
- logger.log('ConnectionMonitor recorded disconnect')
59
- }
60
-
61
- // Private
62
-
63
- startPolling() {
64
- this.stopPolling()
65
- this.poll()
66
- }
67
-
68
- stopPolling() {
69
- clearTimeout(this.pollTimeout)
70
- }
71
-
72
- poll() {
73
- this.pollTimeout = setTimeout(() => {
74
- this.reconnectIfStale()
75
- this.poll()
76
- }, this.getPollInterval())
77
- }
78
-
79
- getPollInterval() {
80
- const { staleThreshold, reconnectionBackoffRate } = this.constructor
81
- const backoff = (1 + reconnectionBackoffRate) ** Math.min(this.reconnectAttempts, 10)
82
- const jitterMax = this.reconnectAttempts === 0 ? 1.0 : reconnectionBackoffRate
83
- const jitter = jitterMax * Math.random()
84
- return staleThreshold * 1000 * backoff * (1 + jitter)
85
- }
86
-
87
- reconnectIfStale() {
88
- if (this.connectionIsStale()) {
89
- logger.log(
90
- `ConnectionMonitor detected stale connection. reconnectAttempts = ${
91
- this.reconnectAttempts
92
- }, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${
93
- this.constructor.staleThreshold
94
- } s`
95
- )
96
- this.reconnectAttempts++
97
- if (this.disconnectedRecently()) {
98
- logger.log(
99
- `ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(
100
- this.disconnectedAt
101
- )} s`
102
- )
103
- } else {
104
- logger.log('ConnectionMonitor reopening')
105
- this.connection.reopen()
106
- }
107
- }
108
- }
109
-
110
- get refreshedAt() {
111
- return this.pingedAt ? this.pingedAt : this.startedAt
112
- }
113
-
114
- connectionIsStale() {
115
- return secondsSince(this.refreshedAt) > this.constructor.staleThreshold
116
- }
117
-
118
- disconnectedRecently() {
119
- return (
120
- this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold
121
- )
122
- }
123
-
124
- visibilityDidChange() {
125
- if (document.visibilityState === 'visible') {
126
- setTimeout(() => {
127
- if (this.connectionIsStale() || !this.connection.isOpen()) {
128
- logger.log(
129
- `ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`
130
- )
131
- this.connection.reopen()
132
- }
133
- }, 200)
134
- }
135
- }
136
- }
137
-
138
- ConnectionMonitor.staleThreshold = 6 // Server::Connections::BEAT_INTERVAL * 2 (missed two pings)
139
- ConnectionMonitor.reconnectionBackoffRate = 0.15
140
-
141
- export default ConnectionMonitor
@@ -1,86 +0,0 @@
1
- import Connection from './connection'
2
- import Subscriptions from './subscriptions'
3
-
4
- // The ActionCable.Consumer establishes the connection to a server-side Ruby Connection object. Once established,
5
- // the ActionCable.ConnectionMonitor will ensure that its properly maintained through heartbeats and checking for stale updates.
6
- // The Consumer instance is also the gateway to establishing subscriptions to desired channels through the #createSubscription
7
- // method.
8
- //
9
- // The following example shows how this can be set up:
10
- //
11
- // App = {}
12
- // App.cable = ActionCable.createConsumer("ws://example.com/accounts/1")
13
- // App.appearance = App.cable.subscriptions.create("AppearanceChannel")
14
- //
15
- // For more details on how you'd configure an actual channel subscription, see ActionCable.Subscription.
16
- //
17
- // When a consumer is created, it automatically connects with the server.
18
- //
19
- // To disconnect from the server, call
20
- //
21
- // App.cable.disconnect()
22
- //
23
- // and to restart the connection:
24
- //
25
- // App.cable.connect()
26
- //
27
- // Any channel subscriptions which existed prior to disconnecting will
28
- // automatically resubscribe.
29
-
30
- export class Consumer {
31
- _url: string
32
- subscriptions: Subscriptions
33
- connection: Connection
34
- subprotocols: Array<string>
35
-
36
- constructor(url: string) {
37
- this._url = url
38
- this.subscriptions = new Subscriptions(this)
39
- this.connection = new Connection(this)
40
- this.subprotocols = []
41
- }
42
-
43
- get url() {
44
- return createWebSocketURL(this._url)
45
- }
46
-
47
- send(data: object) {
48
- return this.connection.send(data)
49
- }
50
-
51
- connect() {
52
- return this.connection.open()
53
- }
54
-
55
- disconnect() {
56
- return this.connection.close({ allowReconnect: false })
57
- }
58
-
59
- ensureActiveConnection() {
60
- if (!this.connection.isActive()) {
61
- return this.connection.open()
62
- }
63
- }
64
-
65
- addSubProtocol(subprotocol: string) {
66
- this.subprotocols = [...this.subprotocols, subprotocol]
67
- }
68
- }
69
-
70
- export function createWebSocketURL(url: string | (() => string)): string {
71
- if (typeof url === 'function') {
72
- url = url()
73
- }
74
-
75
- if (url && !/^wss?:/i.test(url)) {
76
- const a = document.createElement('a')
77
- a.href = url
78
- // biome-ignore lint/correctness/noSelfAssign: <explanation>
79
- a.href = a.href
80
- a.protocol = a.protocol.replace('http', 'ws')
81
- return a.href
82
- }
83
- return url
84
- }
85
-
86
- export default Consumer
@@ -1,35 +0,0 @@
1
- 'use client'
2
-
3
- import adapters from './adapters'
4
- import Connection from './connection'
5
- import ConnectionMonitor from './connection_monitor'
6
- import Consumer, { createWebSocketURL } from './consumer'
7
- import INTERNAL from './internal'
8
- import logger from './logger'
9
- import Subscription from './subscription'
10
- import SubscriptionGuarantor from './subscription_guarantor'
11
- import Subscriptions from './subscriptions'
12
-
13
- export {
14
- Connection,
15
- ConnectionMonitor,
16
- Consumer,
17
- INTERNAL,
18
- Subscription,
19
- Subscriptions,
20
- SubscriptionGuarantor,
21
- adapters,
22
- createWebSocketURL,
23
- logger,
24
- }
25
-
26
- export function createConsumer(url = getConfig('url') || INTERNAL.default_mount_path) {
27
- return new Consumer(url)
28
- }
29
-
30
- export function getConfig(name: string) {
31
- const element = document.head.querySelector(`meta[name='action-cable-${name}']`)
32
- if (element) {
33
- return element.getAttribute('content')
34
- }
35
- }
@@ -1,19 +0,0 @@
1
- const INTERNAL = {
2
- message_types: {
3
- welcome: 'welcome',
4
- disconnect: 'disconnect',
5
- ping: 'ping',
6
- confirmation: 'confirm_subscription',
7
- rejection: 'reject_subscription',
8
- },
9
- disconnect_reasons: {
10
- unauthorized: 'unauthorized',
11
- invalid_request: 'invalid_request',
12
- server_restart: 'server_restart',
13
- remote: 'remote',
14
- },
15
- default_mount_path: '/cable',
16
- protocols: ['actioncable-v1-json', 'actioncable-unsupported'],
17
- }
18
-
19
- export default INTERNAL
@@ -1,18 +0,0 @@
1
- import { debugging } from '../../../../configuration'
2
- import adapters from './adapters'
3
-
4
- class Logger {
5
- get enabled() {
6
- return debugging
7
- }
8
-
9
- log(...messages: Array<string>) {
10
- if (adapters.logger && this.enabled) {
11
- messages.push(Date.now().toString())
12
- adapters.logger.log('[ActionCable]', ...messages)
13
- }
14
- }
15
- }
16
-
17
- export const logger = new Logger()
18
- export default logger
@@ -1,45 +0,0 @@
1
- import type { Consumer } from './consumer'
2
-
3
- export type Data = { [id: string]: string | object | null | undefined }
4
-
5
- const extend = (object: Data, properties: Data) => {
6
- if (properties !== null) {
7
- for (const key in properties) {
8
- const value = properties[key]
9
- object[key] = value
10
- }
11
- }
12
- return object
13
- }
14
-
15
- export class Subscription {
16
- consumer: Consumer
17
- identifier: string
18
-
19
- // biome-ignore lint/style/useDefaultParameterLast: <explanation>
20
- constructor(consumer: Consumer, params: Data = {}, mixin: Data) {
21
- this.consumer = consumer
22
- this.identifier = JSON.stringify(params)
23
- extend(this as unknown as Data, mixin)
24
- }
25
-
26
- // Perform a channel action with the optional data passed as an attribute
27
- perform(action: string, data: Data = {}) {
28
- data.action = action
29
- return this.send(data)
30
- }
31
-
32
- send(data: object) {
33
- return this.consumer.send({
34
- command: 'message',
35
- identifier: this.identifier,
36
- data: JSON.stringify(data),
37
- })
38
- }
39
-
40
- unsubscribe() {
41
- return this.consumer.subscriptions.remove(this)
42
- }
43
- }
44
-
45
- export default Subscription
@@ -1,54 +0,0 @@
1
- import logger from './logger'
2
- import type { Subscription } from './subscription'
3
- import type { Subscriptions } from './subscriptions'
4
-
5
- // Responsible for ensuring channel subscribe command is confirmed, retrying until confirmation is received.
6
- // Internal class, not intended for direct user manipulation.
7
-
8
- class SubscriptionGuarantor {
9
- subscriptions: Subscriptions
10
- pendingSubscriptions: Array<Subscription>
11
- retryTimeout: ReturnType<typeof setTimeout> | undefined
12
-
13
- constructor(subscriptions: Subscriptions) {
14
- this.subscriptions = subscriptions
15
- this.pendingSubscriptions = []
16
- }
17
-
18
- guarantee(subscription: Subscription) {
19
- if (this.pendingSubscriptions.indexOf(subscription) === -1) {
20
- logger.log(`SubscriptionGuarantor guaranteeing ${subscription.identifier}`)
21
- this.pendingSubscriptions.push(subscription)
22
- } else {
23
- logger.log(`SubscriptionGuarantor already guaranteeing ${subscription.identifier}`)
24
- }
25
- this.startGuaranteeing()
26
- }
27
-
28
- forget(subscription: Subscription) {
29
- logger.log(`SubscriptionGuarantor forgetting ${subscription.identifier}`)
30
- this.pendingSubscriptions = this.pendingSubscriptions.filter((s) => s !== subscription)
31
- }
32
-
33
- startGuaranteeing() {
34
- this.stopGuaranteeing()
35
- this.retrySubscribing()
36
- }
37
-
38
- stopGuaranteeing() {
39
- clearTimeout(this.retryTimeout)
40
- }
41
-
42
- retrySubscribing() {
43
- this.retryTimeout = setTimeout(() => {
44
- if (this.subscriptions && typeof this.subscriptions.subscribe === 'function') {
45
- this.pendingSubscriptions.map((subscription) => {
46
- logger.log(`SubscriptionGuarantor resubscribing ${subscription.identifier}`)
47
- this.subscriptions.subscribe(subscription)
48
- })
49
- }
50
- }, 500)
51
- }
52
- }
53
-
54
- export default SubscriptionGuarantor
@@ -1,113 +0,0 @@
1
- import type { Consumer } from './consumer'
2
- import logger from './logger'
3
- import type { Data } from './subscription'
4
- import Subscription from './subscription'
5
- import SubscriptionGuarantor from './subscription_guarantor'
6
-
7
- // Collection class for creating (and internally managing) channel subscriptions.
8
- // The only method intended to be triggered by the user is ActionCable.Subscriptions#create,
9
- // and it should be called through the consumer like so:
10
- //
11
- // App = {}
12
- // App.cable = ActionCable.createConsumer("ws://example.com/accounts/1")
13
- // App.appearance = App.cable.subscriptions.create("AppearanceChannel")
14
- //
15
- // For more details on how you'd configure an actual channel subscription, see ActionCable.Subscription.
16
-
17
- export class Subscriptions {
18
- consumer: Consumer
19
- guarantor: SubscriptionGuarantor
20
- subscriptions: Array<Subscription>
21
-
22
- constructor(consumer: Consumer) {
23
- this.consumer = consumer
24
- this.guarantor = new SubscriptionGuarantor(this)
25
- this.subscriptions = []
26
- }
27
-
28
- create(channelName: string, mixin: Data) {
29
- const channel = channelName
30
- const params = typeof channel === 'object' ? channel : { channel }
31
- const subscription = new Subscription(this.consumer, params, mixin)
32
- return this.add(subscription)
33
- }
34
-
35
- // Private
36
-
37
- add(subscription: Subscription) {
38
- this.subscriptions.push(subscription)
39
- this.consumer.ensureActiveConnection()
40
- this.notify(subscription, 'initialized')
41
- this.subscribe(subscription)
42
- return subscription
43
- }
44
-
45
- remove(subscription: Subscription) {
46
- this.forget(subscription)
47
- if (!this.findAll(subscription.identifier).length) {
48
- this.sendCommand(subscription, 'unsubscribe')
49
- }
50
- return subscription
51
- }
52
-
53
- reject(identifier: string) {
54
- return this.findAll(identifier).map((subscription) => {
55
- this.forget(subscription)
56
- this.notify(subscription, 'rejected')
57
- return subscription
58
- })
59
- }
60
-
61
- forget(subscription: Subscription) {
62
- this.guarantor.forget(subscription)
63
- this.subscriptions = this.subscriptions.filter((s) => s !== subscription)
64
- return subscription
65
- }
66
-
67
- findAll(identifier: string) {
68
- return this.subscriptions.filter((s) => s.identifier === identifier)
69
- }
70
-
71
- reload() {
72
- return this.subscriptions.map((subscription) => this.subscribe(subscription))
73
- }
74
-
75
- notifyAll(callbackName: string, ...args: any[]) {
76
- return this.subscriptions.map((subscription) =>
77
- this.notify(subscription, callbackName, ...args)
78
- )
79
- }
80
-
81
- notify(subscription: Subscription, callbackName: string, ...args: any[]) {
82
- let subscriptions: Array<Subscription>
83
- if (typeof subscription === 'string') {
84
- subscriptions = this.findAll(subscription)
85
- } else {
86
- subscriptions = [subscription]
87
- }
88
-
89
- return subscriptions.map((subscription: any) =>
90
- typeof subscription[callbackName] === 'function'
91
- ? subscription[callbackName](...args)
92
- : undefined
93
- )
94
- }
95
-
96
- subscribe(subscription: Subscription) {
97
- if (this.sendCommand(subscription, 'subscribe')) {
98
- this.guarantor.guarantee(subscription)
99
- }
100
- }
101
-
102
- confirmSubscription(identifier: string) {
103
- logger.log(`Subscription confirmed ${identifier}`)
104
- this.findAll(identifier).map((subscription) => this.guarantor.forget(subscription))
105
- }
106
-
107
- sendCommand(subscription: Subscription, command: string) {
108
- const { identifier } = subscription
109
- return this.consumer.send({ command, identifier })
110
- }
111
- }
112
-
113
- export default Subscriptions