Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions javascript_client/src/subscriptions/ActionCableLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,46 @@ type SubscriptionCallbacks = {
received?: (payload: any) => void;
};

type CreateChannelId = () => string

function createChannelId() {
// `crypto.randomUUID()` (Web Crypto API) is used because a low-entropy
// identifier here can collide between simultaneously-created subscriptions,
// and ActionCable routes incoming payloads by identifier — colliding
// subscriptions would receive each other's payloads.
return crypto.randomUUID()
}

class ActionCableLink extends ApolloLink {
cable: Consumer
channelName: string
actionName: string
connectionParams: ConnectionParams
callbacks: SubscriptionCallbacks
createChannelId: CreateChannelId

constructor(options: {
cable: Consumer, channelName?: string, actionName?: string, connectionParams?: ConnectionParams, callbacks?: SubscriptionCallbacks
cable: Consumer,
createChannelId?: CreateChannelId,
channelName?: string,
actionName?: string,
connectionParams?: ConnectionParams,
callbacks?: SubscriptionCallbacks,
}) {
super()
this.cable = options.cable
this.channelName = options.channelName || "GraphqlChannel"
this.actionName = options.actionName || "execute"
this.connectionParams = options.connectionParams || {}
this.callbacks = options.callbacks || {}
this.createChannelId = options.createChannelId || createChannelId
}

// Interestingly, this link does _not_ call through to `next` because
// instead, it sends the request to ActionCable.
request(operation: Operation, _next: NextLink): Observable<RequestResult> {
return new Observable((observer) => {
var channelId = Math.round(Date.now() + Math.random() * 100000).toString(16)
var channelId = this.createChannelId()
var actionName = this.actionName
var connectionParams = (typeof this.connectionParams === "function") ?
this.connectionParams(operation) : this.connectionParams
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,31 @@ describe("ActionCableLink", () => {
expect(subscription.params["test"]).toEqual(1)
})

it("generates a unique channelId for each subscription", () => {
var link = new ActionCableLink(options)
var channelIds = new Set<string>()
var subscriptions: any[] = []

for (var i = 0; i < 1000; i++) {
var observable = link.request(operation, null as any)
var subscription: any = (observable.subscribe(() => null) as any)._cleanup
channelIds.add(subscription.params.channelId)
subscriptions.push(subscription)
}

expect(channelIds.size).toBe(1000)

subscriptions.forEach(function(s) { s.unsubscribe() })
})

it("accepts an injected channel ID function", () => {
var link = new ActionCableLink({...options, createChannelId: () => "Channel-ID" })
var observable = link.request(operation, null as any)
var subscription: any = (observable.subscribe(() => null) as any)._cleanup
expect(subscription.params.channelId).toEqual("Channel-ID")
subscription.unsubscribe()
})

it('allows passing custom callbacks', () => {
var connected = jest.fn()
var received = jest.fn()
Expand Down
Loading