Skip to content

Commit

Permalink
feat: support react-native (#97)
Browse files Browse the repository at this point in the history
React-native has a hobbled URL class that seems to make it's own
WebSocket class cause a fatal crash so stringify it before passing
it in.

Otherwise the URL class is supported everywhere now so no need to
use iso-url any more.

Also noteworthy is that react-native doesn't have any node core
modules so we have to use a similar browser override system to ensure
the pure-js/browser APIs are used instead.
  • Loading branch information
achingbrain authored Dec 10, 2023
1 parent 96f4c7b commit 440af16
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 32 deletions.
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,6 @@
"dependencies": {
"@types/ws": "^8.2.2",
"event-iterator": "^2.0.0",
"iso-url": "^1.1.2",
"it-stream-types": "^2.0.1",
"uint8arrays": "^4.0.2",
"ws": "^8.4.0"
Expand All @@ -208,5 +207,12 @@
"ws": false,
"http": false,
"https": false
},
"react-native": {
"./dist/src/web-socket.js": "./dist/src/web-socket.browser.js",
"./dist/src/server.js": false,
"ws": false,
"http": false,
"https": false
}
}
7 changes: 4 additions & 3 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ export interface WebSocketOptions extends SinkOptions {
}

export function connect (addr: string, opts?: WebSocketOptions): DuplexWebSocket {
const location = typeof window === 'undefined' ? '' : window.location
const location = typeof window === 'undefined' ? undefined : window.location
opts = opts ?? {}

const url = wsurl(addr, location.toString())
const socket = new WebSocket(url, opts.websocket)
const url = wsurl(addr, location)

// it's necessary to stringify the URL object otherwise react-native crashes
const socket = new WebSocket(url.toString(), opts.websocket)
return duplex(socket, opts)
}
27 changes: 23 additions & 4 deletions src/ws-url.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
import { relative } from 'iso-url'
const map = { 'http:': 'ws:', 'https:': 'wss:' }
const defaultProtocol = 'ws:'

const map = { http: 'ws', https: 'wss' }
const def = 'ws'
export default (url: string, location?: Partial<Location>): URL => {
if (url.startsWith('//')) {
url = `${location?.protocol ?? defaultProtocol}${url}`
}

export default (url: string, location: string | Partial<Location>): string => relative(url, location, map, def)
if (url.startsWith('/') && location != null) {
const proto = location.protocol ?? defaultProtocol
const host = location.host
const port = location.port != null && host?.endsWith(`:${location.port}`) !== true ? `:${location.port}` : ''
url = `${proto}//${host}${port}${url}`
}

const wsUrl = new URL(url)

for (const [httpProto, wsProto] of Object.entries(map)) {
if (wsUrl.protocol === httpProto) {
wsUrl.protocol = wsProto
}
}

return wsUrl
}
45 changes: 32 additions & 13 deletions test/error.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import drain from 'it-drain'
import { pipe } from 'it-pipe'
import defer from 'p-defer'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { isNode } from 'wherearewe'
import * as WS from '../src/index.js'
import WebSocket from '../src/web-socket.js'

Expand Down Expand Up @@ -50,18 +51,36 @@ describe('error', () => {
expect(sinkError.message).to.equal(sourceError.message)
})

it('test connection error awaiting connected', async () => {
await expect(
WS.duplex(new WebSocket(`ws://localhost:34897/${Math.random()}`)).connected()
).to.eventually.be.rejected.with.property('message').that.matches(/ECONNREFUSED/g)
})
// ws under node throws AggregateErrors
if (isNode) {
it('test connection error awaiting connected', async () => {
await expect(
WS.duplex(new WebSocket(`ws://localhost:34897/${Math.random()}`)).connected()
).to.eventually.be.rejected.with.nested.property('errors[0].message').that.matches(/ECONNREFUSED/g)
})

it('test connection error in stream', async function () {
await expect(
pipe(
WS.duplex(new WebSocket(`ws://localhost:34897/${Math.random()}`)).source,
drain
)
).to.eventually.be.rejected.with.property('message').that.matches(/ECONNREFUSED/g)
})
it('test connection error in stream', async function () {
await expect(
pipe(
WS.duplex(new WebSocket(`ws://localhost:34897/${Math.random()}`)).source,
drain
)
).to.eventually.be.rejected.with.nested.property('errors[0].message').that.matches(/ECONNREFUSED/g)
})
} else {
it('test connection error awaiting connected', async () => {
await expect(
WS.duplex(new WebSocket(`ws://localhost:34897/${Math.random()}`)).connected()
).to.eventually.be.rejected.with.property('message').that.matches(/ECONNREFUSED/g)
})

it('test connection error in stream', async function () {
await expect(
pipe(
WS.duplex(new WebSocket(`ws://localhost:34897/${Math.random()}`)).source,
drain
)
).to.eventually.be.rejected.with.property('message').that.matches(/ECONNREFUSED/g)
})
}
})
27 changes: 16 additions & 11 deletions test/ws-url.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,40 @@ import wsurl from '../src/ws-url.js'
describe('ws-url', () => {
it('map from a relative url to one for this domain', () => {
const location = {
protocol: 'http',
protocol: 'http:',
host: 'foo.com',
pathname: '/whatever',
search: '?okay=true'
}

expect(wsurl('//bar.com', location)).to.equal('ws://bar.com/')
expect(wsurl('/this', location)).to.equal('ws://foo.com/this')
expect(wsurl('//bar.com', location).toString()).to.equal('ws://bar.com/')
expect(wsurl('/this', location).toString()).to.equal('ws://foo.com/this')
})

it('same path works on dev and deployed', () => {
expect(wsurl('/', {
protocol: 'http',
protocol: 'http:',
host: 'localhost:8000'
})).to.equal('ws://localhost:8000/')
}).toString()).to.equal('ws://localhost:8000/')
expect(wsurl('/', {
protocol: 'http',
protocol: 'http:',
host: 'server.com:8000'
})).to.equal('ws://server.com:8000/')
}).toString()).to.equal('ws://server.com:8000/')
})

it('universal url still works', () => {
expect(wsurl('ws://what.com/okay', {
protocol: 'http',
protocol: 'http:',
host: 'localhost:8000'
})).to.equal('ws://what.com/okay')
}).toString()).to.equal('ws://what.com/okay')
expect(wsurl('wss://localhost/', {
protocol: 'https',
protocol: 'https:',
host: 'localhost:8000'
})).to.equal('wss://localhost/')
}).toString()).to.equal('wss://localhost/')
expect(wsurl('wss://localhost/', {
protocol: 'https:',
host: 'localhost:8000',
port: '8000'
}).toString()).to.equal('wss://localhost/')
})
})

0 comments on commit 440af16

Please sign in to comment.