@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,139 @@
|
|
|
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
|
+
delete this.stoppedAt
|
|
22
|
+
this.startPolling()
|
|
23
|
+
addEventListener('visibilitychange', this.visibilityDidChange)
|
|
24
|
+
logger.log(
|
|
25
|
+
`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
stop() {
|
|
31
|
+
if (this.isRunning()) {
|
|
32
|
+
this.stoppedAt = now()
|
|
33
|
+
this.stopPolling()
|
|
34
|
+
removeEventListener('visibilitychange', this.visibilityDidChange)
|
|
35
|
+
logger.log('ConnectionMonitor stopped')
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
isRunning() {
|
|
40
|
+
return this.startedAt && !this.stoppedAt
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
recordPing() {
|
|
44
|
+
this.pingedAt = now()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
recordConnect() {
|
|
48
|
+
this.reconnectAttempts = 0
|
|
49
|
+
this.recordPing()
|
|
50
|
+
delete this.disconnectedAt
|
|
51
|
+
logger.log('ConnectionMonitor recorded connect')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
recordDisconnect() {
|
|
55
|
+
this.disconnectedAt = now()
|
|
56
|
+
logger.log('ConnectionMonitor recorded disconnect')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Private
|
|
60
|
+
|
|
61
|
+
startPolling() {
|
|
62
|
+
this.stopPolling()
|
|
63
|
+
this.poll()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
stopPolling() {
|
|
67
|
+
clearTimeout(this.pollTimeout)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
poll() {
|
|
71
|
+
this.pollTimeout = setTimeout(() => {
|
|
72
|
+
this.reconnectIfStale()
|
|
73
|
+
this.poll()
|
|
74
|
+
}, this.getPollInterval())
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getPollInterval() {
|
|
78
|
+
const { staleThreshold, reconnectionBackoffRate } = this.constructor
|
|
79
|
+
const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10))
|
|
80
|
+
const jitterMax = this.reconnectAttempts === 0 ? 1.0 : reconnectionBackoffRate
|
|
81
|
+
const jitter = jitterMax * Math.random()
|
|
82
|
+
return staleThreshold * 1000 * backoff * (1 + jitter)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
reconnectIfStale() {
|
|
86
|
+
if (this.connectionIsStale()) {
|
|
87
|
+
logger.log(
|
|
88
|
+
`ConnectionMonitor detected stale connection. reconnectAttempts = ${
|
|
89
|
+
this.reconnectAttempts
|
|
90
|
+
}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${
|
|
91
|
+
this.constructor.staleThreshold
|
|
92
|
+
} s`
|
|
93
|
+
)
|
|
94
|
+
this.reconnectAttempts++
|
|
95
|
+
if (this.disconnectedRecently()) {
|
|
96
|
+
logger.log(
|
|
97
|
+
`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(
|
|
98
|
+
this.disconnectedAt
|
|
99
|
+
)} s`
|
|
100
|
+
)
|
|
101
|
+
} else {
|
|
102
|
+
logger.log('ConnectionMonitor reopening')
|
|
103
|
+
this.connection.reopen()
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get refreshedAt() {
|
|
109
|
+
return this.pingedAt ? this.pingedAt : this.startedAt
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
connectionIsStale() {
|
|
113
|
+
return secondsSince(this.refreshedAt) > this.constructor.staleThreshold
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
disconnectedRecently() {
|
|
117
|
+
return (
|
|
118
|
+
this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
visibilityDidChange() {
|
|
123
|
+
if (document.visibilityState === 'visible') {
|
|
124
|
+
setTimeout(() => {
|
|
125
|
+
if (this.connectionIsStale() || !this.connection.isOpen()) {
|
|
126
|
+
logger.log(
|
|
127
|
+
`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`
|
|
128
|
+
)
|
|
129
|
+
this.connection.reopen()
|
|
130
|
+
}
|
|
131
|
+
}, 200)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
ConnectionMonitor.staleThreshold = 6 // Server::Connections::BEAT_INTERVAL * 2 (missed two pings)
|
|
137
|
+
ConnectionMonitor.reconnectionBackoffRate = 0.15
|
|
138
|
+
|
|
139
|
+
export default ConnectionMonitor
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
// eslint-disable-next-line no-self-assign
|
|
79
|
+
a.href = a.href
|
|
80
|
+
a.protocol = a.protocol.replace('http', 'ws')
|
|
81
|
+
return a.href
|
|
82
|
+
} else {
|
|
83
|
+
return url
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default Consumer
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { debugging } from '../../../../configuration'
|
|
2
|
+
import adapters from './adapters'
|
|
3
|
+
|
|
4
|
+
class Logger {
|
|
5
|
+
enabled = debugging
|
|
6
|
+
|
|
7
|
+
log(...messages: Array<string>) {
|
|
8
|
+
if (adapters.logger && this.enabled) {
|
|
9
|
+
messages.push(Date.now().toString())
|
|
10
|
+
adapters.logger.log('[ActionCable]', ...messages)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const logger = new Logger()
|
|
16
|
+
export default logger
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Consumer } from './consumer'
|
|
2
|
+
|
|
3
|
+
export type Data = { [id: string]: string | object | null | undefined }
|
|
4
|
+
|
|
5
|
+
const extend = function (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
|
+
constructor(consumer: Consumer, params: Data = {}, mixin: Data) {
|
|
20
|
+
this.consumer = consumer
|
|
21
|
+
this.identifier = JSON.stringify(params)
|
|
22
|
+
extend(this as unknown as Data, mixin)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Perform a channel action with the optional data passed as an attribute
|
|
26
|
+
perform(action: string, data: Data = {}) {
|
|
27
|
+
data.action = action
|
|
28
|
+
return this.send(data)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
send(data: object) {
|
|
32
|
+
return this.consumer.send({
|
|
33
|
+
command: 'message',
|
|
34
|
+
identifier: this.identifier,
|
|
35
|
+
data: JSON.stringify(data),
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
unsubscribe() {
|
|
40
|
+
return this.consumer.subscriptions.remove(this)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default Subscription
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
|
@@ -0,0 +1,113 @@
|
|
|
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
|
|
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
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './AuthLink'
|
|
2
|
+
export * from './BatchHttpLink'
|
|
3
|
+
export * from './ErrorLink'
|
|
4
|
+
export * from './ForwardableLink'
|
|
5
|
+
export * from './HttpLink'
|
|
6
|
+
export * from './RetryLink'
|
|
7
|
+
export * from './SubscriptionLink'
|
|
8
|
+
export * from './TerminatingLink'
|
|
9
|
+
export * from './VersionLink'
|
package/src/api/index.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { AxiosRequestConfig, AxiosResponse } from 'axios'
|
|
2
|
+
import axios from 'axios'
|
|
3
|
+
|
|
4
|
+
import { endpointAuth } from '../../configuration'
|
|
5
|
+
|
|
6
|
+
export enum AuthStrategies {
|
|
7
|
+
Email = 'email',
|
|
8
|
+
Phone = 'phone',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface EmailInput {
|
|
12
|
+
email: string
|
|
13
|
+
phone?: never
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface PhoneInput {
|
|
17
|
+
phone: string
|
|
18
|
+
email?: never
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type UsernamePayload = EmailInput | PhoneInput
|
|
22
|
+
export type PasscodePayload = UsernamePayload & { passcode: string }
|
|
23
|
+
|
|
24
|
+
type SessionData = { token: string }
|
|
25
|
+
type NoContentData = void
|
|
26
|
+
type UnauthorizedData = { message: string; instruction: string }
|
|
27
|
+
export type UnprocessableData = { [attribute: string]: Array<string> }
|
|
28
|
+
|
|
29
|
+
type Ping = SessionData | UnauthorizedData
|
|
30
|
+
type Indentify = SessionData | NoContentData | UnprocessableData
|
|
31
|
+
type Authenticate = SessionData | UnauthorizedData | UnprocessableData
|
|
32
|
+
type Revoke = NoContentData | UnauthorizedData
|
|
33
|
+
|
|
34
|
+
export type SessionResponse = AxiosResponse<SessionData>
|
|
35
|
+
export type UnprocessableResponse = AxiosResponse<UnprocessableData>
|
|
36
|
+
|
|
37
|
+
// https://www.quiltt.dev/api-reference/rest/auth#
|
|
38
|
+
export class AuthAPI {
|
|
39
|
+
clientId: string | undefined
|
|
40
|
+
|
|
41
|
+
constructor(clientId?: string | undefined) {
|
|
42
|
+
this.clientId = clientId
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Response Statuses:
|
|
47
|
+
* - 200: OK -> Session is Valid
|
|
48
|
+
* - 401: Unauthorized -> Session is Invalid
|
|
49
|
+
*/
|
|
50
|
+
ping = (token: string) => {
|
|
51
|
+
return axios.get<Ping>(endpointAuth, this.config(token))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Response Statuses:
|
|
56
|
+
* - 201: Created -> Profile Created, New Session Returned
|
|
57
|
+
* - 202: Accepted -> Profile Found, MFA Code Sent for `authenticate`
|
|
58
|
+
* - 422: Unprocessable Entity -> Invalid Payload
|
|
59
|
+
*/
|
|
60
|
+
identify = (payload: UsernamePayload) => {
|
|
61
|
+
return axios.post<Indentify>(endpointAuth, this.body(payload), this.config())
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Response Statuses:
|
|
66
|
+
* - 201: Created -> MFA Validated, New Session Returned
|
|
67
|
+
* - 401: Unauthorized -> MFA Invalid
|
|
68
|
+
* - 422: Unprocessable Entity -> Invalid Payload
|
|
69
|
+
*/
|
|
70
|
+
authenticate = (payload: PasscodePayload) => {
|
|
71
|
+
return axios.put<Authenticate>(endpointAuth, this.body(payload), this.config())
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Response Statuses:
|
|
76
|
+
* - 204: No Content -> Session Revoked
|
|
77
|
+
* - 401: Unauthorized -> Session Not Found
|
|
78
|
+
*/
|
|
79
|
+
revoke = (token: string) => {
|
|
80
|
+
return axios.delete<Revoke>(endpointAuth, this.config(token))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private config = (token?: string): AxiosRequestConfig => {
|
|
84
|
+
const headers: { [id: string]: string } = {
|
|
85
|
+
'Content-Type': 'application/json',
|
|
86
|
+
Accept: 'application/json',
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (token) {
|
|
90
|
+
headers.Authorization = `Bearer ${token}`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
headers: headers,
|
|
95
|
+
validateStatus: this.validateStatus,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private validateStatus = (status: number) => status < 500
|
|
100
|
+
|
|
101
|
+
private body = (payload: any) => {
|
|
102
|
+
if (!this.clientId) {
|
|
103
|
+
console.error('Quiltt Client ID is not set. Unable to identify & authenticate')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
session: {
|
|
108
|
+
deploymentId: this.clientId, // Rename API?
|
|
109
|
+
...payload,
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export default AuthAPI
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './AuthAPI'
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { name as packageName, version as packageVersion } from '../package.json'
|
|
2
|
+
|
|
3
|
+
const QUILTT_API_INSECURE = (() => {
|
|
4
|
+
try {
|
|
5
|
+
return process.env.QUILTT_API_INSECURE
|
|
6
|
+
} catch {
|
|
7
|
+
return undefined
|
|
8
|
+
}
|
|
9
|
+
})()
|
|
10
|
+
|
|
11
|
+
const QUILTT_API_DOMAIN = (() => {
|
|
12
|
+
try {
|
|
13
|
+
return process.env.QUILTT_API_DOMAIN
|
|
14
|
+
} catch {
|
|
15
|
+
return undefined
|
|
16
|
+
}
|
|
17
|
+
})()
|
|
18
|
+
|
|
19
|
+
const QUILTT_DEBUG = (() => {
|
|
20
|
+
try {
|
|
21
|
+
return !!process.env.QUILTT_DEBUG || process.env.NODE_ENV !== 'production'
|
|
22
|
+
} catch {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
})()
|
|
26
|
+
|
|
27
|
+
const domain = QUILTT_API_DOMAIN || 'quiltt.io'
|
|
28
|
+
const protocolHttp = `http${QUILTT_API_INSECURE ? '' : 's'}`
|
|
29
|
+
const protocolWebsockets = `ws${QUILTT_API_DOMAIN ? '' : 's'}`
|
|
30
|
+
|
|
31
|
+
export const debugging = QUILTT_DEBUG
|
|
32
|
+
export const endpointAuth = `${protocolHttp}://auth.${domain}/v1/users/session`
|
|
33
|
+
export const endpointGraphQL = `${protocolHttp}://api.${domain}/v1/graphql`
|
|
34
|
+
export const endpointWebsockets = `${protocolWebsockets}://api.${domain}/websockets`
|
|
35
|
+
export const version = `${packageName}: v${packageVersion}`
|
|
36
|
+
|
|
37
|
+
const Config = {
|
|
38
|
+
debugging,
|
|
39
|
+
endpointAuth,
|
|
40
|
+
endpointGraphQL,
|
|
41
|
+
endpointWebsockets,
|
|
42
|
+
version,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default Config
|