@quiltt/core 2.1.0 → 2.1.2
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.
- package/CHANGELOG.md +352 -0
- package/README.md +32 -9
- package/dist/{VersionLink-2203a632.d.ts → VersionLink-b7dfbaba.d.ts} +1 -1
- package/dist/api/graphql/index.cjs +4 -4
- package/dist/api/graphql/index.cjs.map +1 -1
- package/dist/api/graphql/index.d.ts +1 -1
- package/dist/api/graphql/index.js +3 -3
- package/dist/api/graphql/index.js.map +1 -1
- package/dist/api/graphql/links/actioncable/index.cjs +4 -4
- package/dist/api/graphql/links/actioncable/index.cjs.map +1 -1
- package/dist/api/graphql/links/actioncable/index.js +2 -2
- package/dist/api/graphql/links/actioncable/index.js.map +1 -1
- package/dist/api/graphql/links/index.cjs +2 -2
- package/dist/api/graphql/links/index.cjs.map +1 -1
- package/dist/api/graphql/links/index.d.ts +1 -1
- package/dist/api/graphql/links/index.js +2 -2
- package/dist/api/graphql/links/index.js.map +1 -1
- package/dist/api/index.cjs +1 -1
- package/dist/api/index.cjs.map +1 -1
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.js +1 -1
- package/dist/api/index.js.map +1 -1
- package/dist/api/rest/index.cjs +1 -1
- package/dist/api/rest/index.cjs.map +1 -1
- package/dist/api/rest/index.js +1 -1
- package/dist/api/rest/index.js.map +1 -1
- package/dist/index.cjs +5 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +10 -2
- package/src/JsonWebToken.ts +48 -0
- package/src/Observable.ts +39 -0
- package/src/Storage/Local.ts +92 -0
- package/src/Storage/Memory.ts +43 -0
- package/src/Storage/index.ts +93 -0
- package/src/Timeoutable.ts +32 -0
- package/src/api/graphql/client.ts +48 -0
- package/src/api/graphql/index.ts +2 -0
- package/src/api/graphql/links/ActionCableLink.ts +93 -0
- package/src/api/graphql/links/AuthLink.ts +32 -0
- package/src/api/graphql/links/BatchHttpLink.ts +12 -0
- package/src/api/graphql/links/ErrorLink.ts +23 -0
- package/src/api/graphql/links/ForwardableLink.ts +5 -0
- package/src/api/graphql/links/HttpLink.ts +12 -0
- package/src/api/graphql/links/RetryLink.ts +10 -0
- package/src/api/graphql/links/SubscriptionLink.ts +11 -0
- package/src/api/graphql/links/TerminatingLink.ts +5 -0
- package/src/api/graphql/links/VersionLink.ts +15 -0
- package/src/api/graphql/links/actioncable/adapters.ts +4 -0
- package/src/api/graphql/links/actioncable/connection.ts +191 -0
- package/src/api/graphql/links/actioncable/connection_monitor.ts +139 -0
- package/src/api/graphql/links/actioncable/consumer.ts +87 -0
- package/src/api/graphql/links/actioncable/index.ts +35 -0
- package/src/api/graphql/links/actioncable/internal.ts +19 -0
- package/src/api/graphql/links/actioncable/logger.ts +16 -0
- package/src/api/graphql/links/actioncable/subscription.ts +44 -0
- package/src/api/graphql/links/actioncable/subscription_guarantor.ts +54 -0
- package/src/api/graphql/links/actioncable/subscriptions.ts +113 -0
- package/src/api/graphql/links/index.ts +9 -0
- package/src/api/index.ts +2 -0
- package/src/api/rest/AuthAPI.ts +115 -0
- package/src/api/rest/index.ts +1 -0
- package/src/configuration.ts +45 -0
- package/src/index.ts +6 -0
- package/src/types.ts +10 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Maybe } from '../types'
|
|
2
|
+
|
|
3
|
+
import type { Observer } from '../Observable'
|
|
4
|
+
import { LocalStorage } from './Local'
|
|
5
|
+
import { MemoryStorage } from './Memory'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* This is wraps both local and memory storage to create a unified interface, that
|
|
9
|
+
* allows you to subscribe to all either changes made within this window, or changes
|
|
10
|
+
* made by other windows.
|
|
11
|
+
*/
|
|
12
|
+
export class Storage<T> {
|
|
13
|
+
private memoryStore = new MemoryStorage<T>()
|
|
14
|
+
private localStore = new LocalStorage<T>()
|
|
15
|
+
private observers: { [key: string]: Observer<T>[] } = {}
|
|
16
|
+
private monitors: Set<string> = new Set()
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Checks memoryStorage before falling back to localStorage.
|
|
20
|
+
*/
|
|
21
|
+
get = (key: string): Maybe<T> | undefined => {
|
|
22
|
+
this.monitorLocalStorageChanges(key)
|
|
23
|
+
|
|
24
|
+
let state = this.memoryStore.get(key)
|
|
25
|
+
|
|
26
|
+
if (state === undefined) {
|
|
27
|
+
state = this.localStore.get(key)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return state
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* We don't trust localStorage to always be present, so we can't rely on it to
|
|
35
|
+
* update memoryStorage based on emitted changes. So we manage our own
|
|
36
|
+
* emitting while using the underlying events to keep memoryStore in sync with
|
|
37
|
+
* localStore.
|
|
38
|
+
*/
|
|
39
|
+
set = (key: string, newState: Maybe<T> | undefined) => {
|
|
40
|
+
this.monitorLocalStorageChanges(key)
|
|
41
|
+
|
|
42
|
+
this.localStore.set(key, newState)
|
|
43
|
+
this.memoryStore.set(key, newState)
|
|
44
|
+
|
|
45
|
+
this.observers[key]?.forEach((update) => update(newState))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Allows you to subscribe to all changes in memory or local storage as a
|
|
50
|
+
* single event.
|
|
51
|
+
*/
|
|
52
|
+
subscribe = (key: string, observer: Observer<T>) => {
|
|
53
|
+
if (!this.observers[key]) this.observers[key] = []
|
|
54
|
+
|
|
55
|
+
this.observers[key].push(observer)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
unsubscribe = (key: string, observer: Observer<T>) => {
|
|
59
|
+
this.observers[key] = this.observers[key]?.filter((update) => update !== observer)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Sets bubble the changes down the stack starting with memoryStore and then
|
|
64
|
+
* localStore. memoryStore will emit changes to everything within the current
|
|
65
|
+
* window context, while localStore will emit changes to every other window
|
|
66
|
+
* context.
|
|
67
|
+
*
|
|
68
|
+
* To ensure that the other windows are updated correctly, changes to localStore
|
|
69
|
+
* need to be subscribed and updated to in memory store, which then may be subscribed
|
|
70
|
+
* to outside of storage.
|
|
71
|
+
*/
|
|
72
|
+
private monitorLocalStorageChanges = (key: string) => {
|
|
73
|
+
if (this.monitors.has(key)) return
|
|
74
|
+
this.monitors.add(key)
|
|
75
|
+
|
|
76
|
+
this.localStore.subscribe(key, (nextState) => {
|
|
77
|
+
const prevValue = this.memoryStore.get(key)
|
|
78
|
+
const newState = nextState instanceof Function ? nextState(prevValue) : nextState
|
|
79
|
+
|
|
80
|
+
this.memoryStore.set(key, newState)
|
|
81
|
+
this.observers[key]?.forEach((update) => update(newState))
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export * from './Local'
|
|
87
|
+
export * from './Memory'
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* This is an singleton to share the memory states across all instances; This
|
|
91
|
+
* basically acts like shared memory when there is no localStorage.
|
|
92
|
+
*/
|
|
93
|
+
export const GlobalStorage = new Storage<any>()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Observer } from './Observable'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This is designed to support singletons to timeouts that can broadcast
|
|
5
|
+
* to any observers, preventing race conditions with multiple timeouts.
|
|
6
|
+
*/
|
|
7
|
+
export class Timeoutable {
|
|
8
|
+
private timeout?: ReturnType<typeof setTimeout>
|
|
9
|
+
private observers: Observer<void>[] = []
|
|
10
|
+
|
|
11
|
+
set = (callback: () => void, delay: number | undefined) => {
|
|
12
|
+
if (this.timeout) {
|
|
13
|
+
clearTimeout(this.timeout)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
this.observers.push(callback)
|
|
17
|
+
this.timeout = setTimeout(this.broadcast.bind(this), delay)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
clear = (observer: Observer<void>) => {
|
|
21
|
+
this.observers = this.observers.filter((callback) => callback !== observer)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Only sends to the 1st listener, but ensures that someone is notified
|
|
25
|
+
private broadcast = () => {
|
|
26
|
+
if (this.observers.length === 0) return
|
|
27
|
+
|
|
28
|
+
this.observers[0](undefined)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default Timeoutable
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ApolloClientOptions, Operation } from '@apollo/client/index.js'
|
|
2
|
+
import { ApolloClient, ApolloLink } from '@apollo/client/index.js'
|
|
3
|
+
|
|
4
|
+
import { debugging } from '../../configuration'
|
|
5
|
+
import {
|
|
6
|
+
AuthLink,
|
|
7
|
+
BatchHttpLink,
|
|
8
|
+
ErrorLink,
|
|
9
|
+
ForwardableLink,
|
|
10
|
+
HttpLink,
|
|
11
|
+
RetryLink,
|
|
12
|
+
SubscriptionLink,
|
|
13
|
+
VersionLink,
|
|
14
|
+
} from './links'
|
|
15
|
+
|
|
16
|
+
export type QuilttClientOptions<T> = Omit<ApolloClientOptions<T>, 'link'>
|
|
17
|
+
|
|
18
|
+
export class QuilttClient<T> extends ApolloClient<T> {
|
|
19
|
+
constructor(options: QuilttClientOptions<T>) {
|
|
20
|
+
if (!options.connectToDevTools) options.connectToDevTools = debugging
|
|
21
|
+
|
|
22
|
+
const isSubscriptionOperation = (operation: Operation) => {
|
|
23
|
+
return operation.query.definitions.some(
|
|
24
|
+
// @ts-ignore
|
|
25
|
+
({ kind, operation }) => kind === 'OperationDefinition' && operation === 'subscription'
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const isBatchable = (operation: Operation) => {
|
|
30
|
+
return operation.getContext().batchable ?? true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const authLink = new AuthLink()
|
|
34
|
+
const subscriptionsLink = new SubscriptionLink()
|
|
35
|
+
|
|
36
|
+
const quilttLink = ApolloLink.from([VersionLink, authLink, ErrorLink, RetryLink])
|
|
37
|
+
.split(isSubscriptionOperation, subscriptionsLink, ForwardableLink)
|
|
38
|
+
.split(isBatchable, BatchHttpLink, HttpLink)
|
|
39
|
+
|
|
40
|
+
super({
|
|
41
|
+
link: quilttLink,
|
|
42
|
+
...options,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { InMemoryCache, gql, useMutation, useQuery, useSubscription } from '@apollo/client/index.js'
|
|
48
|
+
export type { NormalizedCacheObject, OperationVariables } from '@apollo/client/index.js'
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { GlobalStorage } from '@/Storage'
|
|
2
|
+
import type { FetchResult, NextLink, Operation } from '@apollo/client/core/index.js'
|
|
3
|
+
import { ApolloLink, Observable } from '@apollo/client/core/index.js'
|
|
4
|
+
import { print } from 'graphql'
|
|
5
|
+
import { endpointWebsockets } from '../../../configuration'
|
|
6
|
+
import type { Consumer } from './actioncable'
|
|
7
|
+
import { createConsumer } from './actioncable'
|
|
8
|
+
|
|
9
|
+
type RequestResult = FetchResult<
|
|
10
|
+
{ [key: string]: unknown },
|
|
11
|
+
Record<string, unknown>,
|
|
12
|
+
Record<string, unknown>
|
|
13
|
+
>
|
|
14
|
+
type ConnectionParams = object | ((operation: Operation) => object)
|
|
15
|
+
|
|
16
|
+
class ActionCableLink extends ApolloLink {
|
|
17
|
+
cables: { [id: string]: Consumer }
|
|
18
|
+
channelName: string
|
|
19
|
+
actionName: string
|
|
20
|
+
connectionParams: ConnectionParams
|
|
21
|
+
|
|
22
|
+
constructor(options: {
|
|
23
|
+
channelName?: string
|
|
24
|
+
actionName?: string
|
|
25
|
+
connectionParams?: ConnectionParams
|
|
26
|
+
}) {
|
|
27
|
+
super()
|
|
28
|
+
this.cables = {}
|
|
29
|
+
this.channelName = options.channelName || 'GraphqlChannel'
|
|
30
|
+
this.actionName = options.actionName || 'execute'
|
|
31
|
+
this.connectionParams = options.connectionParams || {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Interestingly, this link does _not_ call through to `next` because
|
|
35
|
+
// instead, it sends the request to ActionCable.
|
|
36
|
+
request(operation: Operation, _next: NextLink): Observable<RequestResult> | null {
|
|
37
|
+
const token = GlobalStorage.get('session')
|
|
38
|
+
|
|
39
|
+
if (!token) {
|
|
40
|
+
console.warn(`QuilttLink attempted to send an unauthenticated Subscription`)
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!this.cables[token]) {
|
|
45
|
+
this.cables[token] = createConsumer(endpointWebsockets + (token ? `?token=${token}` : ''))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return new Observable((observer) => {
|
|
49
|
+
const channelId = Math.round(Date.now() + Math.random() * 100000).toString(16)
|
|
50
|
+
const actionName = this.actionName
|
|
51
|
+
const connectionParams =
|
|
52
|
+
typeof this.connectionParams === 'function'
|
|
53
|
+
? this.connectionParams(operation)
|
|
54
|
+
: this.connectionParams
|
|
55
|
+
|
|
56
|
+
const channel = this.cables[token].subscriptions.create(
|
|
57
|
+
Object.assign(
|
|
58
|
+
{},
|
|
59
|
+
{
|
|
60
|
+
channel: this.channelName,
|
|
61
|
+
channelId: channelId,
|
|
62
|
+
},
|
|
63
|
+
connectionParams
|
|
64
|
+
),
|
|
65
|
+
{
|
|
66
|
+
connected: () => {
|
|
67
|
+
channel.perform(actionName, {
|
|
68
|
+
query: operation.query ? print(operation.query) : null,
|
|
69
|
+
variables: operation.variables,
|
|
70
|
+
// This is added for persisted operation support:
|
|
71
|
+
operationId: (operation as { operationId?: string }).operationId,
|
|
72
|
+
operationName: operation.operationName,
|
|
73
|
+
})
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
received: (payload: { result: RequestResult; more: any }) => {
|
|
77
|
+
if (payload?.result?.data || payload?.result?.errors) {
|
|
78
|
+
observer.next(payload.result)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!payload.more) {
|
|
82
|
+
observer.complete()
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
// Make the ActionCable subscription behave like an Apollo subscription
|
|
88
|
+
return Object.assign(channel, { closed: false })
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default ActionCableLink
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { FetchResult, NextLink, Observable, Operation } from '@apollo/client/index.js'
|
|
2
|
+
import { ApolloLink } from '@apollo/client/index.js'
|
|
3
|
+
|
|
4
|
+
import { GlobalStorage } from '@/Storage'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* unauthorizedCallback only triggers in the event the token is present, and
|
|
8
|
+
* returns the token; This allows sessions to be forgotten without race conditions
|
|
9
|
+
* causing null sessions to kill valid sessions, or invalid sessions for killing
|
|
10
|
+
* valid sessions during rotation and networking weirdness.
|
|
11
|
+
*/
|
|
12
|
+
export class AuthLink extends ApolloLink {
|
|
13
|
+
request(operation: Operation, forward: NextLink): Observable<FetchResult> | null {
|
|
14
|
+
const token = GlobalStorage.get('session')
|
|
15
|
+
|
|
16
|
+
if (!token) {
|
|
17
|
+
console.warn(`QuilttLink attempted to send an unauthenticated Query`)
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
operation.setContext(({ headers = {} }) => ({
|
|
22
|
+
headers: {
|
|
23
|
+
...headers,
|
|
24
|
+
authorization: `Bearer ${token}`,
|
|
25
|
+
},
|
|
26
|
+
}))
|
|
27
|
+
|
|
28
|
+
return forward(operation)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default AuthLink
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BatchHttpLink as ApolloHttpLink } from '@apollo/client/link/batch-http/index.js'
|
|
2
|
+
|
|
3
|
+
import fetch from 'cross-fetch'
|
|
4
|
+
|
|
5
|
+
import { endpointGraphQL } from '../../../configuration'
|
|
6
|
+
|
|
7
|
+
export const BatchHttpLink = new ApolloHttpLink({
|
|
8
|
+
uri: endpointGraphQL,
|
|
9
|
+
fetch,
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
export default BatchHttpLink
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { GlobalStorage } from '@/Storage'
|
|
2
|
+
|
|
3
|
+
import type { ServerError } from '@apollo/client/index.js'
|
|
4
|
+
import { onError } from '@apollo/client/link/error/index.js'
|
|
5
|
+
|
|
6
|
+
export const ErrorLink = onError(({ graphQLErrors, networkError }) => {
|
|
7
|
+
if (graphQLErrors) {
|
|
8
|
+
graphQLErrors.forEach(({ message, locations, path }) => {
|
|
9
|
+
console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`)
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (networkError) {
|
|
14
|
+
if ((networkError as ServerError).statusCode === 401) {
|
|
15
|
+
console.warn('[Authentication error]:', networkError)
|
|
16
|
+
GlobalStorage.set('session', null)
|
|
17
|
+
} else {
|
|
18
|
+
console.warn('[Network error]:', networkError)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export default ErrorLink
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { HttpLink as ApolloHttpLink } from '@apollo/client/link/http/index.js'
|
|
2
|
+
|
|
3
|
+
import fetch from 'cross-fetch'
|
|
4
|
+
|
|
5
|
+
import { endpointGraphQL } from '../../../configuration'
|
|
6
|
+
|
|
7
|
+
export const HttpLink = new ApolloHttpLink({
|
|
8
|
+
uri: endpointGraphQL,
|
|
9
|
+
fetch,
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
export default HttpLink
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ServerError } from '@apollo/client/index.js'
|
|
2
|
+
import { RetryLink as ApolloRetryLink } from '@apollo/client/link/retry/index.js'
|
|
3
|
+
|
|
4
|
+
export const RetryLink = new ApolloRetryLink({
|
|
5
|
+
attempts: {
|
|
6
|
+
retryIf: (error: ServerError, _operation) => error.statusCode >= 500,
|
|
7
|
+
},
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export default RetryLink
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ApolloLink } from '@apollo/client/index.js'
|
|
2
|
+
|
|
3
|
+
import { version } from '../../../configuration'
|
|
4
|
+
|
|
5
|
+
export const VersionLink = new ApolloLink((operation, forward) => {
|
|
6
|
+
operation.setContext(({ headers = {} }) => ({
|
|
7
|
+
headers: {
|
|
8
|
+
...headers,
|
|
9
|
+
'Quiltt-Client-Version': version,
|
|
10
|
+
},
|
|
11
|
+
}))
|
|
12
|
+
return forward(operation)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export default VersionLink
|
|
@@ -0,0 +1,191 @@
|
|
|
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
|
+
} else {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
open() {
|
|
33
|
+
if (this.isActive()) {
|
|
34
|
+
logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`)
|
|
35
|
+
return false
|
|
36
|
+
} else {
|
|
37
|
+
const socketProtocols = [...protocols, ...(this.consumer.subprotocols || [])]
|
|
38
|
+
logger.log(
|
|
39
|
+
`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${socketProtocols}`
|
|
40
|
+
)
|
|
41
|
+
if (this.webSocket) {
|
|
42
|
+
this.uninstallEventHandlers()
|
|
43
|
+
}
|
|
44
|
+
this.webSocket = new adapters.WebSocket(this.consumer.url, socketProtocols)
|
|
45
|
+
this.installEventHandlers()
|
|
46
|
+
this.monitor.start()
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
close({ allowReconnect } = { allowReconnect: true }) {
|
|
52
|
+
if (!allowReconnect) {
|
|
53
|
+
this.monitor.stop()
|
|
54
|
+
}
|
|
55
|
+
// Avoid closing websockets in a "connecting" state due to Safari 15.1+ bug. See: https://github.com/rails/rails/issues/43835#issuecomment-1002288478
|
|
56
|
+
if (this.isOpen()) {
|
|
57
|
+
return this.webSocket.close()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
reopen() {
|
|
62
|
+
logger.log(`Reopening WebSocket, current state is ${this.getState()}`)
|
|
63
|
+
if (this.isActive()) {
|
|
64
|
+
try {
|
|
65
|
+
return this.close()
|
|
66
|
+
} catch (error) {
|
|
67
|
+
logger.log('Failed to reopen WebSocket', error)
|
|
68
|
+
} finally {
|
|
69
|
+
logger.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`)
|
|
70
|
+
setTimeout(this.open, this.constructor.reopenDelay)
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
return this.open()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getProtocol() {
|
|
78
|
+
if (this.webSocket) {
|
|
79
|
+
return this.webSocket.protocol
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
isOpen() {
|
|
84
|
+
return this.isState('open')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
isActive() {
|
|
88
|
+
return this.isState('open', 'connecting')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
triedToReconnect() {
|
|
92
|
+
return this.monitor.reconnectAttempts > 0
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Private
|
|
96
|
+
|
|
97
|
+
isProtocolSupported() {
|
|
98
|
+
return indexOf.call(supportedProtocols, this.getProtocol()) >= 0
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
isState(...states) {
|
|
102
|
+
return indexOf.call(states, this.getState()) >= 0
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
getState() {
|
|
106
|
+
if (this.webSocket) {
|
|
107
|
+
for (const state in adapters.WebSocket) {
|
|
108
|
+
if (adapters.WebSocket[state] === this.webSocket.readyState) {
|
|
109
|
+
return state.toLowerCase()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
installEventHandlers() {
|
|
117
|
+
for (const eventName in this.events) {
|
|
118
|
+
const handler = this.events[eventName].bind(this)
|
|
119
|
+
this.webSocket[`on${eventName}`] = handler
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
uninstallEventHandlers() {
|
|
124
|
+
for (const eventName in this.events) {
|
|
125
|
+
this.webSocket[`on${eventName}`] = function () {}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
Connection.reopenDelay = 500
|
|
131
|
+
|
|
132
|
+
Connection.prototype.events = {
|
|
133
|
+
message(event) {
|
|
134
|
+
if (!this.isProtocolSupported()) {
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
const { identifier, message, reason, reconnect, type } = JSON.parse(event.data)
|
|
138
|
+
switch (type) {
|
|
139
|
+
case message_types.welcome:
|
|
140
|
+
if (this.triedToReconnect()) {
|
|
141
|
+
this.reconnectAttempted = true
|
|
142
|
+
}
|
|
143
|
+
this.monitor.recordConnect()
|
|
144
|
+
return this.subscriptions.reload()
|
|
145
|
+
case message_types.disconnect:
|
|
146
|
+
logger.log(`Disconnecting. Reason: ${reason}`)
|
|
147
|
+
return this.close({ allowReconnect: reconnect })
|
|
148
|
+
case message_types.ping:
|
|
149
|
+
return this.monitor.recordPing()
|
|
150
|
+
case message_types.confirmation:
|
|
151
|
+
this.subscriptions.confirmSubscription(identifier)
|
|
152
|
+
if (this.reconnectAttempted) {
|
|
153
|
+
this.reconnectAttempted = false
|
|
154
|
+
return this.subscriptions.notify(identifier, 'connected', { reconnected: true })
|
|
155
|
+
} else {
|
|
156
|
+
return this.subscriptions.notify(identifier, 'connected', { reconnected: false })
|
|
157
|
+
}
|
|
158
|
+
case message_types.rejection:
|
|
159
|
+
return this.subscriptions.reject(identifier)
|
|
160
|
+
default:
|
|
161
|
+
return this.subscriptions.notify(identifier, 'received', message)
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
open() {
|
|
166
|
+
logger.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`)
|
|
167
|
+
this.disconnected = false
|
|
168
|
+
if (!this.isProtocolSupported()) {
|
|
169
|
+
logger.log('Protocol is unsupported. Stopping monitor and disconnecting.')
|
|
170
|
+
return this.close({ allowReconnect: false })
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
close(_event) {
|
|
175
|
+
logger.log('WebSocket onclose event')
|
|
176
|
+
if (this.disconnected) {
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
this.disconnected = true
|
|
180
|
+
this.monitor.recordDisconnect()
|
|
181
|
+
return this.subscriptions.notifyAll('disconnected', {
|
|
182
|
+
willAttemptReconnect: this.monitor.isRunning(),
|
|
183
|
+
})
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
error() {
|
|
187
|
+
logger.log('WebSocket onerror event')
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export default Connection
|