Init
This commit is contained in:
+22
@@ -0,0 +1,22 @@
|
||||
# MIT License
|
||||
|
||||
Copyright (c) 2014 Chris McCord
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./priv/static/phoenix-orange.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./priv/static/phoenix.png" />
|
||||
<img src="./priv/static/phoenix.png" alt="Phoenix logo" />
|
||||
</picture>
|
||||
|
||||
> Peace of mind from prototype to production.
|
||||
|
||||
[](https://github.com/supabase/phoenix/actions/workflows/ci.yml) [](https://www.npmjs.com/package/@supabase/phoenix)
|
||||
|
||||
## Supabase Fork
|
||||
|
||||
This is a Supabase fork of Phoenix Framework, published to npm as `@supabase/phoenix`.
|
||||
|
||||
**Installation:**
|
||||
```bash
|
||||
npm install @supabase/phoenix
|
||||
```
|
||||
|
||||
**Releases**: This fork uses automated releases via [release-please](https://github.com/googleapis/release-please). See [RELEASE.md](RELEASE.md) for details.
|
||||
|
||||
**Upstream**: Based on [phoenixframework/phoenix](https://github.com/phoenixframework/phoenix)
|
||||
|
||||
## Versioning
|
||||
|
||||
This package uses **independent semantic versioning** for the JavaScript client.
|
||||
|
||||
- **Based on**: Phoenix Framework 1.8.3 JS client
|
||||
- **Last synced**: 2026-07-09
|
||||
|
||||
We version based on **JS API changes only**, not upstream Phoenix framework releases.
|
||||
When we merge upstream Phoenix changes, we evaluate the JS API impact and version accordingly.
|
||||
|
||||
## Getting started
|
||||
|
||||
See the official site at <https://www.phoenixframework.org/>.
|
||||
|
||||
Install the latest version of Phoenix by following the instructions at <https://phoenix.hexdocs.pm/installation.html#phoenix>.
|
||||
|
||||
## Documentation
|
||||
|
||||
API documentation is available at <https://phoenix.hexdocs.pm>.
|
||||
|
||||
Phoenix.js documentation is available at <https://phoenix.hexdocs.pm/js>.
|
||||
|
||||
## Contributing
|
||||
|
||||
We appreciate any contribution to Phoenix. Check our [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) and [CONTRIBUTING.md](CONTRIBUTING.md) guides for more information. We usually keep a list of features and bugs in the [issue tracker][4].
|
||||
|
||||
### Generating a Phoenix project from unreleased versions
|
||||
|
||||
You can create a new project using the latest Phoenix source installer (the `phx.new` Mix task) with the following steps:
|
||||
|
||||
1. Remove any previously installed `phx_new` archives so that Mix will pick up the local source code. This can be done with `mix archive.uninstall phx_new` or by simply deleting the file, which is usually in `~/.mix/archives/`.
|
||||
2. Copy this repo via `git clone https://github.com/phoenixframework/phoenix` or by downloading it
|
||||
3. Run the `phx.new` Mix task from within the `installer` directory, for example:
|
||||
|
||||
```bash
|
||||
cd phoenix/installer
|
||||
mix phx.new dev_app --dev
|
||||
```
|
||||
|
||||
The `--dev` flag will configure your new project's `:phoenix` dep as a relative path dependency, pointing to your local Phoenix checkout:
|
||||
|
||||
```elixir
|
||||
defp deps do
|
||||
[{:phoenix, path: "../..", override: true},
|
||||
```
|
||||
|
||||
To create projects outside of the `installer/` directory, add the latest archive to your machine by following the instructions in [installer/README.md](https://github.com/phoenixframework/phoenix/blob/main/installer/README.md)
|
||||
|
||||
### Building from source
|
||||
|
||||
To build the documentation:
|
||||
|
||||
```bash
|
||||
MIX_ENV=docs mix docs
|
||||
```
|
||||
|
||||
To build Phoenix:
|
||||
|
||||
```bash
|
||||
mix deps.get
|
||||
mix compile
|
||||
```
|
||||
|
||||
To build the Phoenix installer:
|
||||
|
||||
```bash
|
||||
mix deps.get
|
||||
mix compile
|
||||
mix archive.build
|
||||
```
|
||||
|
||||
To build Phoenix.js:
|
||||
|
||||
```bash
|
||||
mix assets.build
|
||||
```
|
||||
|
||||
## Important links
|
||||
|
||||
* [#elixir][1] on [Libera][2] IRC
|
||||
* [elixir-lang Slack channel][3]
|
||||
* [Issues tracker][4]
|
||||
* [Phoenix Forum (questions and proposals)][5]
|
||||
* Visit Phoenix's sponsor, DockYard, for expert [Phoenix Consulting](https://dockyard.com/phoenix-consulting)
|
||||
|
||||
[1]: https://web.libera.chat/?channels=#elixir
|
||||
[2]: https://libera.chat/
|
||||
[3]: https://elixir-lang.slack.com/
|
||||
[4]: https://github.com/phoenixframework/phoenix/issues
|
||||
[5]: https://elixirforum.com/c/phoenix-forum
|
||||
|
||||
## Copyright and License
|
||||
|
||||
Copyright (c) 2014, Chris McCord.
|
||||
|
||||
Phoenix source code is licensed under the [MIT License](LICENSE.md).
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
import {
|
||||
global,
|
||||
XHR_STATES
|
||||
} from "./constants"
|
||||
|
||||
export default class Ajax {
|
||||
|
||||
static request(method, endPoint, headers, body, timeout, ontimeout, callback){
|
||||
if(global.XDomainRequest){
|
||||
let req = new global.XDomainRequest() // IE8, IE9
|
||||
return this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback)
|
||||
} else if(global.XMLHttpRequest){
|
||||
let req = new global.XMLHttpRequest() // IE7+, Firefox, Chrome, Opera, Safari
|
||||
return this.xhrRequest(req, method, endPoint, headers, body, timeout, ontimeout, callback)
|
||||
} else if(global.fetch && global.AbortController){
|
||||
// Fetch with AbortController for modern browsers
|
||||
return this.fetchRequest(method, endPoint, headers, body, timeout, ontimeout, callback)
|
||||
} else {
|
||||
throw new Error("No suitable XMLHttpRequest implementation found")
|
||||
}
|
||||
}
|
||||
|
||||
static fetchRequest(method, endPoint, headers, body, timeout, ontimeout, callback){
|
||||
let options = {
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
}
|
||||
let controller = null
|
||||
if(timeout){
|
||||
controller = new AbortController()
|
||||
const _timeoutId = setTimeout(() => controller.abort(), timeout)
|
||||
options.signal = controller.signal
|
||||
}
|
||||
global.fetch(endPoint, options)
|
||||
.then(response => response.text())
|
||||
.then(data => this.parseJSON(data))
|
||||
.then(data => callback && callback(data))
|
||||
.catch(err => {
|
||||
if(err.name === "AbortError" && ontimeout){
|
||||
ontimeout()
|
||||
} else {
|
||||
callback && callback(null)
|
||||
}
|
||||
})
|
||||
return controller
|
||||
}
|
||||
|
||||
static xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback){
|
||||
req.timeout = timeout
|
||||
req.open(method, endPoint)
|
||||
req.onload = () => {
|
||||
let response = this.parseJSON(req.responseText)
|
||||
callback && callback(response)
|
||||
}
|
||||
if(ontimeout){ req.ontimeout = ontimeout }
|
||||
|
||||
// Work around bug in IE9 that requires an attached onprogress handler
|
||||
req.onprogress = () => { }
|
||||
|
||||
req.send(body)
|
||||
return req
|
||||
}
|
||||
|
||||
static xhrRequest(req, method, endPoint, headers, body, timeout, ontimeout, callback){
|
||||
req.open(method, endPoint, true)
|
||||
req.timeout = timeout
|
||||
for(let [key, value] of Object.entries(headers)){
|
||||
req.setRequestHeader(key, value)
|
||||
}
|
||||
req.onerror = () => callback && callback(null)
|
||||
req.onreadystatechange = () => {
|
||||
if(req.readyState === XHR_STATES.complete && callback){
|
||||
let response = this.parseJSON(req.responseText)
|
||||
callback(response)
|
||||
}
|
||||
}
|
||||
if(ontimeout){ req.ontimeout = ontimeout }
|
||||
|
||||
req.send(body)
|
||||
return req
|
||||
}
|
||||
|
||||
static parseJSON(resp){
|
||||
if(!resp || resp === ""){ return null }
|
||||
|
||||
try {
|
||||
return JSON.parse(resp)
|
||||
} catch {
|
||||
console && console.log("failed to parse JSON response", resp)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
static serialize(obj, parentKey){
|
||||
let queryStr = []
|
||||
for(var key in obj){
|
||||
if(!Object.prototype.hasOwnProperty.call(obj, key)){ continue }
|
||||
let paramKey = parentKey ? `${parentKey}[${key}]` : key
|
||||
let paramVal = obj[key]
|
||||
if(typeof paramVal === "object"){
|
||||
queryStr.push(this.serialize(paramVal, paramKey))
|
||||
} else {
|
||||
queryStr.push(encodeURIComponent(paramKey) + "=" + encodeURIComponent(paramVal))
|
||||
}
|
||||
}
|
||||
return queryStr.join("&")
|
||||
}
|
||||
|
||||
static appendParams(url, params){
|
||||
if(Object.keys(params).length === 0){ return url }
|
||||
|
||||
let prefix = url.match(/\?/) ? "&" : "?"
|
||||
return `${url}${prefix}${this.serialize(params)}`
|
||||
}
|
||||
}
|
||||
+331
@@ -0,0 +1,331 @@
|
||||
import {closure} from "./utils"
|
||||
import {
|
||||
CHANNEL_EVENTS,
|
||||
CHANNEL_STATES,
|
||||
} from "./constants"
|
||||
|
||||
import Push from "./push"
|
||||
import Timer from "./timer"
|
||||
|
||||
/**
|
||||
* @import Socket from "./socket"
|
||||
* @import { ChannelState, Params, ChannelBindingCallback, ChannelOnMessage, ChannelFilterBindings, ChannelOnErrorCallback, ChannelBinding } from "./types"
|
||||
*/
|
||||
|
||||
export default class Channel {
|
||||
/**
|
||||
* @param {string} topic
|
||||
* @param {Params | (() => Params)} params
|
||||
* @param {Socket} socket
|
||||
*/
|
||||
constructor(topic, params, socket){
|
||||
/** @type{ChannelState} */
|
||||
this.state = CHANNEL_STATES.closed
|
||||
/** @type{string} */
|
||||
this.topic = topic
|
||||
/** @type{() => Params} */
|
||||
this.params = closure(params || {})
|
||||
/** @type {Socket} */
|
||||
this.socket = socket
|
||||
/** @type{ChannelBinding[]} */
|
||||
this.bindings = []
|
||||
/** @type{number} */
|
||||
this.bindingRef = 0
|
||||
/** @type{number} */
|
||||
this.timeout = this.socket.timeout
|
||||
/** @type{boolean} */
|
||||
this.joinedOnce = false
|
||||
/** @type{Push} */
|
||||
this.joinPush = new Push(this, CHANNEL_EVENTS.join, this.params, this.timeout)
|
||||
/** @type{Push[]} */
|
||||
this.pushBuffer = []
|
||||
/** @type{string[]} */
|
||||
this.stateChangeRefs = []
|
||||
|
||||
/** @type{Timer} */
|
||||
this.rejoinTimer = new Timer(() => {
|
||||
if(this.socket.isConnected()){ this.rejoin() }
|
||||
}, this.socket.rejoinAfterMs)
|
||||
this.stateChangeRefs.push(this.socket.onError(() => this.rejoinTimer.reset()))
|
||||
this.stateChangeRefs.push(this.socket.onOpen(() => {
|
||||
this.rejoinTimer.reset()
|
||||
if(this.isErrored()){ this.rejoin() }
|
||||
})
|
||||
)
|
||||
this.joinPush.receive("ok", () => {
|
||||
this.state = CHANNEL_STATES.joined
|
||||
this.rejoinTimer.reset()
|
||||
this.pushBuffer.forEach(pushEvent => pushEvent.send())
|
||||
this.pushBuffer = []
|
||||
})
|
||||
this.joinPush.receive("error", (reason) => {
|
||||
this.state = CHANNEL_STATES.errored
|
||||
if(this.socket.hasLogger()) this.socket.log("channel", `error ${this.topic}`, reason)
|
||||
if(this.socket.isConnected()){ this.rejoinTimer.scheduleTimeout() }
|
||||
})
|
||||
this.onClose(() => {
|
||||
this.rejoinTimer.reset()
|
||||
if(this.socket.hasLogger()) this.socket.log("channel", `close ${this.topic}`)
|
||||
this.state = CHANNEL_STATES.closed
|
||||
this.socket.remove(this)
|
||||
})
|
||||
this.onError(reason => {
|
||||
if(this.socket.hasLogger()) this.socket.log("channel", `error ${this.topic}`, reason)
|
||||
if(this.isJoining()){ this.joinPush.reset() }
|
||||
this.state = CHANNEL_STATES.errored
|
||||
if(this.socket.isConnected()){ this.rejoinTimer.scheduleTimeout() }
|
||||
})
|
||||
this.joinPush.receive("timeout", () => {
|
||||
if(this.socket.hasLogger()) this.socket.log("channel", `timeout ${this.topic}`, this.joinPush.timeout)
|
||||
let leavePush = new Push(this, CHANNEL_EVENTS.leave, closure({}), this.timeout)
|
||||
leavePush.send()
|
||||
this.state = CHANNEL_STATES.errored
|
||||
this.joinPush.reset()
|
||||
if(this.socket.isConnected()){ this.rejoinTimer.scheduleTimeout() }
|
||||
})
|
||||
this.on(CHANNEL_EVENTS.reply, (payload, ref) => {
|
||||
this.trigger(this.replyEventName(ref), payload)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Join the channel
|
||||
* @param {number} timeout
|
||||
* @returns {Push}
|
||||
*/
|
||||
join(timeout = this.timeout){
|
||||
if(this.joinedOnce){
|
||||
throw new Error("tried to join multiple times. 'join' can only be called a single time per channel instance")
|
||||
} else {
|
||||
this.timeout = timeout
|
||||
this.joinedOnce = true
|
||||
this.rejoin()
|
||||
return this.joinPush
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Teardown the channel.
|
||||
*
|
||||
* Destroys and stops related timers.
|
||||
*/
|
||||
teardown(){
|
||||
this.pushBuffer.forEach((push) => push.destroy())
|
||||
this.pushBuffer = []
|
||||
this.rejoinTimer.reset()
|
||||
this.joinPush.destroy()
|
||||
this.state = CHANNEL_STATES.closed
|
||||
this.bindings = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into channel close
|
||||
* @param {ChannelBindingCallback} callback
|
||||
*/
|
||||
onClose(callback){
|
||||
this.on(CHANNEL_EVENTS.close, callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into channel errors
|
||||
* @param {ChannelOnErrorCallback} callback
|
||||
* @return {number}
|
||||
*/
|
||||
onError(callback){
|
||||
return this.on(CHANNEL_EVENTS.error, reason => callback(reason))
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes on channel events
|
||||
*
|
||||
* Subscription returns a ref counter, which can be used later to
|
||||
* unsubscribe the exact event listener
|
||||
*
|
||||
* @example
|
||||
* const ref1 = channel.on("event", do_stuff)
|
||||
* const ref2 = channel.on("event", do_other_stuff)
|
||||
* channel.off("event", ref1)
|
||||
* // Since unsubscription, do_stuff won't fire,
|
||||
* // while do_other_stuff will keep firing on the "event"
|
||||
*
|
||||
* @param {string} event
|
||||
* @param {ChannelBindingCallback} callback
|
||||
* @returns {number} ref
|
||||
*/
|
||||
on(event, callback){
|
||||
let ref = this.bindingRef++
|
||||
this.bindings.push({event, ref, callback})
|
||||
return ref
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes off of channel events
|
||||
*
|
||||
* Use the ref returned from a channel.on() to unsubscribe one
|
||||
* handler, or pass nothing for the ref to unsubscribe all
|
||||
* handlers for the given event.
|
||||
*
|
||||
* @example
|
||||
* // Unsubscribe the do_stuff handler
|
||||
* const ref1 = channel.on("event", do_stuff)
|
||||
* channel.off("event", ref1)
|
||||
*
|
||||
* // Unsubscribe all handlers from event
|
||||
* channel.off("event")
|
||||
*
|
||||
* @param {string} event
|
||||
* @param {number} [ref]
|
||||
*/
|
||||
off(event, ref){
|
||||
this.bindings = this.bindings.filter((bind) => {
|
||||
return !(bind.event === event && (typeof ref === "undefined" || ref === bind.ref))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
canPush(){ return this.socket.isConnected() && this.isJoined() }
|
||||
|
||||
/**
|
||||
* Sends a message `event` to phoenix with the payload `payload`.
|
||||
* Phoenix receives this in the `handle_in(event, payload, socket)`
|
||||
* function. if phoenix replies or it times out (default 10000ms),
|
||||
* then optionally the reply can be received.
|
||||
*
|
||||
* @example
|
||||
* channel.push("event")
|
||||
* .receive("ok", payload => console.log("phoenix replied:", payload))
|
||||
* .receive("error", err => console.log("phoenix errored", err))
|
||||
* .receive("timeout", () => console.log("timed out pushing"))
|
||||
* @param {string} event
|
||||
* @param {Object} payload
|
||||
* @param {number} [timeout]
|
||||
* @returns {Push}
|
||||
*/
|
||||
push(event, payload, timeout = this.timeout){
|
||||
payload = payload || {}
|
||||
if(!this.joinedOnce){
|
||||
throw new Error(`tried to push '${event}' to '${this.topic}' before joining. Use channel.join() before pushing events`)
|
||||
}
|
||||
let pushEvent = new Push(this, event, function (){ return payload }, timeout)
|
||||
if(this.canPush()){
|
||||
pushEvent.send()
|
||||
} else {
|
||||
pushEvent.startTimeout()
|
||||
this.pushBuffer.push(pushEvent)
|
||||
}
|
||||
|
||||
return pushEvent
|
||||
}
|
||||
|
||||
/** Leaves the channel
|
||||
*
|
||||
* Unsubscribes from server events, and
|
||||
* instructs channel to terminate on server
|
||||
*
|
||||
* Triggers onClose() hooks
|
||||
*
|
||||
* To receive leave acknowledgements, use the `receive`
|
||||
* hook to bind to the server ack, ie:
|
||||
*
|
||||
* @example
|
||||
* channel.leave().receive("ok", () => alert("left!") )
|
||||
*
|
||||
* @param {number} timeout
|
||||
* @returns {Push}
|
||||
*/
|
||||
leave(timeout = this.timeout){
|
||||
this.rejoinTimer.reset()
|
||||
this.joinPush.cancelTimeout()
|
||||
|
||||
this.state = CHANNEL_STATES.leaving
|
||||
let onClose = () => {
|
||||
if(this.socket.hasLogger()) this.socket.log("channel", `leave ${this.topic}`)
|
||||
this.trigger(CHANNEL_EVENTS.close, "leave")
|
||||
}
|
||||
let leavePush = new Push(this, CHANNEL_EVENTS.leave, closure({}), timeout)
|
||||
leavePush.receive("ok", () => onClose())
|
||||
.receive("timeout", () => onClose())
|
||||
leavePush.send()
|
||||
if(!this.canPush()){ leavePush.trigger("ok", {}) }
|
||||
|
||||
return leavePush
|
||||
}
|
||||
|
||||
/**
|
||||
* Overridable message hook
|
||||
*
|
||||
* Receives all events for specialized message handling
|
||||
* before dispatching to the channel callbacks.
|
||||
*
|
||||
* Must return the payload, modified or unmodified
|
||||
* @type{ChannelOnMessage}
|
||||
*/
|
||||
onMessage(_event, payload, _ref){ return payload }
|
||||
|
||||
/**
|
||||
* Overridable filter hook
|
||||
*
|
||||
* If this function returns `true`, `binding`'s callback will be called.
|
||||
*
|
||||
* @type{ChannelFilterBindings}
|
||||
*/
|
||||
filterBindings(_binding, _payload, _ref){ return true }
|
||||
|
||||
isMember(topic, event, payload, joinRef){
|
||||
if(this.topic !== topic){ return false }
|
||||
|
||||
if(joinRef && joinRef !== this.joinRef()){
|
||||
if(this.socket.hasLogger()) this.socket.log("channel", "dropping outdated message", {topic, event, payload, joinRef})
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
joinRef(){ return this.joinPush.ref }
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
rejoin(timeout = this.timeout){
|
||||
if(this.isLeaving()){ return }
|
||||
this.socket.leaveOpenTopic(this.topic)
|
||||
this.state = CHANNEL_STATES.joining
|
||||
this.joinPush.resend(timeout)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} event
|
||||
* @param {unknown} [payload]
|
||||
* @param {?string} [ref]
|
||||
* @param {?string} [joinRef]
|
||||
*/
|
||||
trigger(event, payload, ref, joinRef){
|
||||
let handledPayload = this.onMessage(event, payload, ref, joinRef)
|
||||
if(payload && !handledPayload){ throw new Error("channel onMessage callbacks must return the payload, modified or unmodified") }
|
||||
|
||||
let eventBindings = this.bindings.filter(bind => bind.event === event && this.filterBindings(bind, payload, ref))
|
||||
|
||||
for(let i = 0; i < eventBindings.length; i++){
|
||||
let bind = eventBindings[i]
|
||||
bind.callback(handledPayload, ref, joinRef || this.joinRef())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} ref
|
||||
*/
|
||||
replyEventName(ref){ return `chan_reply_${ref}` }
|
||||
|
||||
isClosed(){ return this.state === CHANNEL_STATES.closed }
|
||||
|
||||
isErrored(){ return this.state === CHANNEL_STATES.errored }
|
||||
|
||||
isJoined(){ return this.state === CHANNEL_STATES.joined }
|
||||
|
||||
isJoining(){ return this.state === CHANNEL_STATES.joining }
|
||||
|
||||
isLeaving(){ return this.state === CHANNEL_STATES.leaving }
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
export const globalSelf = typeof self !== "undefined" ? self : null
|
||||
export const phxWindow = typeof window !== "undefined" ? window : null
|
||||
export const global = globalSelf || phxWindow || globalThis
|
||||
export const DEFAULT_VSN = "2.0.0"
|
||||
export const DEFAULT_TIMEOUT = 10000
|
||||
export const WS_CLOSE_NORMAL = 1000
|
||||
export const MAX_LONGPOLL_BATCH_SIZE = 100
|
||||
|
||||
export const SOCKET_STATES = /** @type {const} */ ({connecting: 0, open: 1, closing: 2, closed: 3})
|
||||
|
||||
export const CHANNEL_STATES = /** @type {const} */ ({
|
||||
closed: "closed",
|
||||
errored: "errored",
|
||||
joined: "joined",
|
||||
joining: "joining",
|
||||
leaving: "leaving",
|
||||
})
|
||||
|
||||
export const CHANNEL_EVENTS = /** @type {const} */ ({
|
||||
close: "phx_close",
|
||||
error: "phx_error",
|
||||
join: "phx_join",
|
||||
reply: "phx_reply",
|
||||
leave: "phx_leave"
|
||||
})
|
||||
|
||||
export const TRANSPORTS = /** @type {const} */ ({
|
||||
longpoll: "longpoll",
|
||||
websocket: "websocket"
|
||||
})
|
||||
|
||||
export const XHR_STATES = /** @type {const} */ ({
|
||||
complete: 4
|
||||
})
|
||||
|
||||
export const AUTH_TOKEN_PREFIX = "base64url.bearer.phx."
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* Phoenix Channels JavaScript client
|
||||
*
|
||||
* ## Socket Connection
|
||||
*
|
||||
* A single connection is established to the server and
|
||||
* channels are multiplexed over the connection.
|
||||
* Connect to the server using the `Socket` class:
|
||||
*
|
||||
* ```javascript
|
||||
* let socket = new Socket("/socket", {params: {userToken: "123"}})
|
||||
* socket.connect()
|
||||
* ```
|
||||
*
|
||||
* The `Socket` constructor takes the mount point of the socket,
|
||||
* the authentication params, as well as options that can be found in
|
||||
* the Socket docs, such as configuring the `LongPoll` transport, and
|
||||
* heartbeat.
|
||||
*
|
||||
* ## Channels
|
||||
*
|
||||
* Channels are isolated, concurrent processes on the server that
|
||||
* subscribe to topics and broker events between the client and server.
|
||||
* To join a channel, you must provide the topic, and channel params for
|
||||
* authorization. Here's an example chat room example where `"new_msg"`
|
||||
* events are listened for, messages are pushed to the server, and
|
||||
* the channel is joined with ok/error/timeout matches:
|
||||
*
|
||||
* ```
|
||||
* let channel = socket.channel("room:123", {token: roomToken})
|
||||
* channel.on("new_msg", msg => console.log("Got message", msg) )
|
||||
* $input.onEnter( e => {
|
||||
* channel.push("new_msg", {body: e.target.val}, 10000)
|
||||
* .receive("ok", (msg) => console.log("created message", msg) )
|
||||
* .receive("error", (reasons) => console.log("create failed", reasons) )
|
||||
* .receive("timeout", () => console.log("Networking issue...") )
|
||||
* })
|
||||
*
|
||||
* channel.join()
|
||||
* .receive("ok", ({messages}) => console.log("catching up", messages) )
|
||||
* .receive("error", ({reason}) => console.log("failed join", reason) )
|
||||
* .receive("timeout", () => console.log("Networking issue. Still waiting..."))
|
||||
*```
|
||||
*
|
||||
* ## Joining
|
||||
*
|
||||
* Creating a channel with `socket.channel(topic, params)`, binds the params to
|
||||
* `channel.params`, which are sent up on `channel.join()`.
|
||||
* Subsequent rejoins will send up the modified params for
|
||||
* updating authorization params, or passing up last_message_id information.
|
||||
* Successful joins receive an "ok" status, while unsuccessful joins
|
||||
* receive "error".
|
||||
*
|
||||
* With the default serializers and WebSocket transport, JSON text frames are
|
||||
* used for pushing a JSON object literal. If an `ArrayBuffer` instance is provided,
|
||||
* binary encoding will be used and the message will be sent with the binary
|
||||
* opcode.
|
||||
*
|
||||
* *Note*: binary messages are only supported on the WebSocket transport.
|
||||
*
|
||||
* ## Duplicate Join Subscriptions
|
||||
*
|
||||
* While the client may join any number of topics on any number of channels,
|
||||
* the client may only hold a single subscription for each unique topic at any
|
||||
* given time. When attempting to create a duplicate subscription,
|
||||
* the server will close the existing channel, log a warning, and
|
||||
* spawn a new channel for the topic. The client will have their
|
||||
* `channel.onClose` callbacks fired for the existing channel, and the new
|
||||
* channel join will have its receive hooks processed as normal.
|
||||
*
|
||||
* ## Pushing Messages
|
||||
*
|
||||
* From the previous example, we can see that pushing messages to the server
|
||||
* can be done with `channel.push(eventName, payload)` and we can optionally
|
||||
* receive responses from the push. Additionally, we can use
|
||||
* `receive("timeout", callback)` to abort waiting for our other `receive` hooks
|
||||
* and take action after some period of waiting. The default timeout is 10000ms.
|
||||
*
|
||||
*
|
||||
* ## Socket Hooks
|
||||
*
|
||||
* Lifecycle events of the multiplexed connection can be hooked into via
|
||||
* `socket.onError()` and `socket.onClose()` events, ie:
|
||||
*
|
||||
* ```
|
||||
* socket.onError( () => console.log("there was an error with the connection!") )
|
||||
* socket.onClose( () => console.log("the connection dropped") )
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* ## Channel Hooks
|
||||
*
|
||||
* For each joined channel, you can bind to `onError` and `onClose` events
|
||||
* to monitor the channel lifecycle, ie:
|
||||
*
|
||||
* ```
|
||||
* channel.onError( () => console.log("there was an error!") )
|
||||
* channel.onClose( () => console.log("the channel has gone away gracefully") )
|
||||
* ```
|
||||
*
|
||||
* ### onError hooks
|
||||
*
|
||||
* `onError` hooks are invoked if the socket connection drops, or the channel
|
||||
* crashes on the server. In either case, a channel rejoin is attempted
|
||||
* automatically in an exponential backoff manner.
|
||||
*
|
||||
* ### onClose hooks
|
||||
*
|
||||
* `onClose` hooks are invoked only in two cases. 1) the channel explicitly
|
||||
* closed on the server, or 2). The client explicitly closed, by calling
|
||||
* `channel.leave()`
|
||||
*
|
||||
*
|
||||
* ## Presence
|
||||
*
|
||||
* The `Presence` object provides features for syncing presence information
|
||||
* from the server with the client and handling presences joining and leaving.
|
||||
*
|
||||
* ### Syncing state from the server
|
||||
*
|
||||
* To sync presence state from the server, first instantiate an object and
|
||||
* pass your channel in to track lifecycle events:
|
||||
*
|
||||
* ```
|
||||
* let channel = socket.channel("some:topic")
|
||||
* let presence = new Presence(channel)
|
||||
* ```
|
||||
*
|
||||
* Next, use the `presence.onSync` callback to react to state changes
|
||||
* from the server. For example, to render the list of users every time
|
||||
* the list changes, you could write:
|
||||
*
|
||||
* ```
|
||||
* presence.onSync(() => {
|
||||
* myRenderUsersFunction(presence.list())
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* ### Listing Presences
|
||||
*
|
||||
* `presence.list` is used to return a list of presence information
|
||||
* based on the local state of metadata. By default, all presence
|
||||
* metadata is returned, but a `listBy` function can be supplied to
|
||||
* allow the client to select which metadata to use for a given presence.
|
||||
* For example, you may have a user online from different devices with
|
||||
* a metadata status of "online", but they have set themselves to "away"
|
||||
* on another device. In this case, the app may choose to use the "away"
|
||||
* status for what appears on the UI. The example below defines a `listBy`
|
||||
* function which prioritizes the first metadata which was registered for
|
||||
* each user. This could be the first tab they opened, or the first device
|
||||
* they came online from:
|
||||
*
|
||||
* ```
|
||||
* let listBy = (id, {metas: [first, ...rest]}) => {
|
||||
* first.count = rest.length + 1 // count of this user's presences
|
||||
* first.id = id
|
||||
* return first
|
||||
* }
|
||||
* let onlineUsers = presence.list(listBy)
|
||||
* ```
|
||||
*
|
||||
* ### Handling individual presence join and leave events
|
||||
*
|
||||
* The `presence.onJoin` and `presence.onLeave` callbacks can be used to
|
||||
* react to individual presences joining and leaving the app. For example:
|
||||
*
|
||||
* ```
|
||||
* let presence = new Presence(channel)
|
||||
*
|
||||
* // detect if user has joined for the 1st time or from another tab/device
|
||||
* presence.onJoin((id, current, newPres) => {
|
||||
* if(!current){
|
||||
* console.log("user has entered for the first time", newPres)
|
||||
* } else {
|
||||
* console.log("user additional presence", newPres)
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* // detect if user has left from all tabs/devices, or is still present
|
||||
* presence.onLeave((id, current, leftPres) => {
|
||||
* if(current.metas.length === 0){
|
||||
* console.log("user has left from all devices", leftPres)
|
||||
* } else {
|
||||
* console.log("user left from a device", leftPres)
|
||||
* }
|
||||
* })
|
||||
* // receive presence data from server
|
||||
* presence.onSync(() => {
|
||||
* displayUsers(presence.list())
|
||||
* })
|
||||
* ```
|
||||
* @module phoenix
|
||||
*/
|
||||
|
||||
import Channel from "./channel"
|
||||
import LongPoll from "./longpoll"
|
||||
import Presence from "./presence"
|
||||
import Serializer from "./serializer"
|
||||
import Socket from "./socket"
|
||||
import Timer from "./timer"
|
||||
import Push from "./push"
|
||||
export * from "./types"
|
||||
|
||||
export {
|
||||
Channel,
|
||||
LongPoll,
|
||||
Presence,
|
||||
Push,
|
||||
Serializer,
|
||||
Socket,
|
||||
Timer
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
import {
|
||||
SOCKET_STATES,
|
||||
TRANSPORTS,
|
||||
AUTH_TOKEN_PREFIX,
|
||||
MAX_LONGPOLL_BATCH_SIZE
|
||||
} from "./constants"
|
||||
|
||||
import Ajax from "./ajax"
|
||||
|
||||
let arrayBufferToBase64 = (buffer) => {
|
||||
let binary = ""
|
||||
let bytes = new Uint8Array(buffer)
|
||||
let len = bytes.byteLength
|
||||
for(let i = 0; i < len; i++){ binary += String.fromCharCode(bytes[i]) }
|
||||
return btoa(binary)
|
||||
}
|
||||
|
||||
export default class LongPoll {
|
||||
|
||||
constructor(endPoint, protocols){
|
||||
// we only support subprotocols for authToken
|
||||
// ["phoenix", "base64url.bearer.phx.BASE64_ENCODED_TOKEN"]
|
||||
if(protocols && protocols.length === 2 && protocols[1].startsWith(AUTH_TOKEN_PREFIX)){
|
||||
this.authToken = atob(protocols[1].slice(AUTH_TOKEN_PREFIX.length))
|
||||
}
|
||||
this.endPoint = null
|
||||
this.token = null
|
||||
this.skipHeartbeat = true
|
||||
this.reqs = new Set()
|
||||
this.awaitingBatchAck = false
|
||||
this.currentBatch = null
|
||||
this.currentBatchTimer = null
|
||||
this.batchBuffer = []
|
||||
this.onopen = function (){ } // noop
|
||||
this.onerror = function (){ } // noop
|
||||
this.onmessage = function (){ } // noop
|
||||
this.onclose = function (){ } // noop
|
||||
this.pollEndpoint = this.normalizeEndpoint(endPoint)
|
||||
this.readyState = SOCKET_STATES.connecting
|
||||
// we must wait for the caller to finish setting up our callbacks and timeout properties
|
||||
setTimeout(() => this.poll(), 0)
|
||||
}
|
||||
|
||||
normalizeEndpoint(endPoint){
|
||||
return (endPoint
|
||||
.replace("ws://", "http://")
|
||||
.replace("wss://", "https://")
|
||||
.replace(new RegExp("(.*)\/" + TRANSPORTS.websocket), "$1/" + TRANSPORTS.longpoll))
|
||||
}
|
||||
|
||||
endpointURL(){
|
||||
return Ajax.appendParams(this.pollEndpoint, {token: this.token})
|
||||
}
|
||||
|
||||
closeAndRetry(code, reason, wasClean){
|
||||
this.close(code, reason, wasClean)
|
||||
this.readyState = SOCKET_STATES.connecting
|
||||
}
|
||||
|
||||
ontimeout(){
|
||||
this.onerror("timeout")
|
||||
this.closeAndRetry(1005, "timeout", false)
|
||||
}
|
||||
|
||||
isActive(){ return this.readyState === SOCKET_STATES.open || this.readyState === SOCKET_STATES.connecting }
|
||||
|
||||
poll(){
|
||||
const headers = {"Accept": "application/json"}
|
||||
if(this.authToken){
|
||||
headers["X-Phoenix-AuthToken"] = this.authToken
|
||||
}
|
||||
this.ajax("GET", headers, null, () => this.ontimeout(), resp => {
|
||||
if(resp){
|
||||
var {status, token, messages} = resp
|
||||
if(status === 410 && this.token !== null){
|
||||
// In case we already have a token, this means that our existing session
|
||||
// is gone. We fail so that the client rejoins its channels.
|
||||
this.onerror(410)
|
||||
this.closeAndRetry(3410, "session_gone", false)
|
||||
return
|
||||
}
|
||||
this.token = token
|
||||
} else {
|
||||
status = 0
|
||||
}
|
||||
|
||||
switch(status){
|
||||
case 200:
|
||||
messages.forEach(msg => {
|
||||
// Tasks are what things like event handlers, setTimeout callbacks,
|
||||
// promise resolves and more are run within.
|
||||
// In modern browsers, there are two different kinds of tasks,
|
||||
// microtasks and macrotasks.
|
||||
// Microtasks are mainly used for Promises, while macrotasks are
|
||||
// used for everything else.
|
||||
// Microtasks always have priority over macrotasks. If the JS engine
|
||||
// is looking for a task to run, it will always try to empty the
|
||||
// microtask queue before attempting to run anything from the
|
||||
// macrotask queue.
|
||||
//
|
||||
// For the WebSocket transport, messages always arrive in their own
|
||||
// event. This means that if any promises are resolved from within,
|
||||
// their callbacks will always finish execution by the time the
|
||||
// next message event handler is run.
|
||||
//
|
||||
// In order to emulate this behaviour, we need to make sure each
|
||||
// onmessage handler is run within its own macrotask.
|
||||
setTimeout(() => this.onmessage({data: msg}), 0)
|
||||
})
|
||||
this.poll()
|
||||
break
|
||||
case 204:
|
||||
this.poll()
|
||||
break
|
||||
case 410:
|
||||
this.readyState = SOCKET_STATES.open
|
||||
this.onopen({})
|
||||
this.poll()
|
||||
break
|
||||
case 403:
|
||||
this.onerror(403)
|
||||
this.close(1008, "forbidden", false)
|
||||
break
|
||||
case 0:
|
||||
case 500:
|
||||
this.onerror(500)
|
||||
this.closeAndRetry(1011, "internal server error", 500)
|
||||
break
|
||||
default: throw new Error(`unhandled poll status ${status}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// we collect all pushes within the current event loop by
|
||||
// setTimeout 0, which optimizes back-to-back procedural
|
||||
// pushes against an empty buffer
|
||||
|
||||
send(body){
|
||||
if(typeof(body) !== "string"){ body = arrayBufferToBase64(body) }
|
||||
if(this.currentBatch){
|
||||
this.currentBatch.push(body)
|
||||
} else if(this.awaitingBatchAck){
|
||||
this.batchBuffer.push(body)
|
||||
} else {
|
||||
this.currentBatch = [body]
|
||||
this.currentBatchTimer = setTimeout(() => {
|
||||
this.batchSend(this.currentBatch)
|
||||
this.currentBatch = null
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
batchSend(messages, offset = 0){
|
||||
this.awaitingBatchAck = true
|
||||
const next = offset + MAX_LONGPOLL_BATCH_SIZE
|
||||
const batch = messages.slice(offset, next)
|
||||
this.ajax("POST", {"Content-Type": "application/x-ndjson"}, batch.join("\n"), () => this.onerror("timeout"), resp => {
|
||||
if(!resp || resp.status !== 200){
|
||||
this.awaitingBatchAck = false
|
||||
this.onerror(resp && resp.status)
|
||||
this.closeAndRetry(1011, "internal server error", false)
|
||||
} else if(next < messages.length){
|
||||
this.batchSend(messages, next)
|
||||
} else if(this.batchBuffer.length > 0){
|
||||
this.batchSend(this.batchBuffer)
|
||||
this.batchBuffer = []
|
||||
} else {
|
||||
this.awaitingBatchAck = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
close(code, reason, wasClean){
|
||||
for(let req of this.reqs){ req.abort() }
|
||||
this.readyState = SOCKET_STATES.closed
|
||||
let opts = Object.assign({code: 1000, reason: undefined, wasClean: true}, {code, reason, wasClean})
|
||||
this.batchBuffer = []
|
||||
clearTimeout(this.currentBatchTimer)
|
||||
this.currentBatchTimer = null
|
||||
if(typeof(CloseEvent) !== "undefined"){
|
||||
this.onclose(new CloseEvent("close", opts))
|
||||
} else {
|
||||
this.onclose(opts)
|
||||
}
|
||||
}
|
||||
|
||||
ajax(method, headers, body, onCallerTimeout, callback){
|
||||
let req
|
||||
let ontimeout = () => {
|
||||
this.reqs.delete(req)
|
||||
onCallerTimeout()
|
||||
}
|
||||
req = Ajax.request(method, this.endpointURL(), headers, body, this.timeout, ontimeout, resp => {
|
||||
this.reqs.delete(req)
|
||||
if(this.isActive()){ callback(resp) }
|
||||
})
|
||||
this.reqs.add(req)
|
||||
}
|
||||
}
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* @import Channel from "./channel"
|
||||
* @import { PresenceEvents, PresenceOnJoin, PresenceOnLeave, PresenceOnSync, PresenceState, PresenceDiff, PresenceOptions } from "./types"
|
||||
*/
|
||||
export default class Presence {
|
||||
|
||||
/**
|
||||
* Initializes the Presence
|
||||
* @param {Channel} channel - The Channel
|
||||
* @param {PresenceOptions} [opts] - The options, for example `{events: {state: "state", diff: "diff"}}`
|
||||
*/
|
||||
constructor(channel, opts = {}){
|
||||
let events = opts.events || /** @type {PresenceEvents} */ ({state: "presence_state", diff: "presence_diff"})
|
||||
/** @type{Record<string, PresenceState>} */
|
||||
this.state = Object.create(null)
|
||||
/** @type{PresenceDiff[]} */
|
||||
this.pendingDiffs = []
|
||||
/** @type{Channel} */
|
||||
this.channel = channel
|
||||
/** @type{?number} */
|
||||
this.joinRef = null
|
||||
/** @type{({ onJoin: PresenceOnJoin; onLeave: PresenceOnLeave; onSync: PresenceOnSync })} */
|
||||
this.caller = {
|
||||
onJoin: function (){ },
|
||||
onLeave: function (){ },
|
||||
onSync: function (){ }
|
||||
}
|
||||
|
||||
this.channel.on(events.state, newState => {
|
||||
let {onJoin, onLeave, onSync} = this.caller
|
||||
|
||||
this.joinRef = this.channel.joinRef()
|
||||
this.state = Presence.syncState(this.state, newState, onJoin, onLeave)
|
||||
|
||||
this.pendingDiffs.forEach(diff => {
|
||||
this.state = Presence.syncDiff(this.state, diff, onJoin, onLeave)
|
||||
})
|
||||
this.pendingDiffs = []
|
||||
onSync()
|
||||
})
|
||||
|
||||
this.channel.on(events.diff, diff => {
|
||||
let {onJoin, onLeave, onSync} = this.caller
|
||||
|
||||
if(this.inPendingSyncState()){
|
||||
this.pendingDiffs.push(diff)
|
||||
} else {
|
||||
this.state = Presence.syncDiff(this.state, diff, onJoin, onLeave)
|
||||
onSync()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PresenceOnJoin} callback
|
||||
*/
|
||||
onJoin(callback){ this.caller.onJoin = callback }
|
||||
|
||||
/**
|
||||
* @param {PresenceOnLeave} callback
|
||||
*/
|
||||
onLeave(callback){ this.caller.onLeave = callback }
|
||||
|
||||
/**
|
||||
* @param {PresenceOnSync} callback
|
||||
*/
|
||||
onSync(callback){ this.caller.onSync = callback }
|
||||
|
||||
/**
|
||||
* Returns the array of presences, with selected metadata.
|
||||
*
|
||||
* @template [T=PresenceState]
|
||||
* @param {((key: string, obj: PresenceState) => T)} [by]
|
||||
*
|
||||
* @returns {T[]}
|
||||
*/
|
||||
list(by){ return Presence.list(this.state, by) }
|
||||
|
||||
inPendingSyncState(){
|
||||
return !this.joinRef || (this.joinRef !== this.channel.joinRef())
|
||||
}
|
||||
|
||||
// lower-level public static API
|
||||
|
||||
/**
|
||||
* Used to sync the list of presences on the server
|
||||
* with the client's state. An optional `onJoin` and `onLeave` callback can
|
||||
* be provided to react to changes in the client's local presences across
|
||||
* disconnects and reconnects with the server.
|
||||
*
|
||||
* @param {Record<string, PresenceState>} currentState
|
||||
* @param {Record<string, PresenceState>} newState
|
||||
* @param {PresenceOnJoin} onJoin
|
||||
* @param {PresenceOnLeave} onLeave
|
||||
*
|
||||
* @returns {Record<string, PresenceState>}
|
||||
*/
|
||||
static syncState(currentState, newState, onJoin, onLeave){
|
||||
let state = this.toNullProtoObj(this.clone(currentState))
|
||||
newState = this.toNullProtoObj(newState)
|
||||
let joins = Object.create(null)
|
||||
let leaves = Object.create(null)
|
||||
|
||||
this.map(state, (key, presence) => {
|
||||
if(!newState[key]){
|
||||
leaves[key] = presence
|
||||
}
|
||||
})
|
||||
this.map(newState, (key, newPresence) => {
|
||||
let currentPresence = state[key]
|
||||
if(currentPresence){
|
||||
let newRefs = newPresence.metas.map(m => m.phx_ref)
|
||||
let curRefs = currentPresence.metas.map(m => m.phx_ref)
|
||||
let joinedMetas = newPresence.metas.filter(m => curRefs.indexOf(m.phx_ref) < 0)
|
||||
let leftMetas = currentPresence.metas.filter(m => newRefs.indexOf(m.phx_ref) < 0)
|
||||
if(joinedMetas.length > 0){
|
||||
joins[key] = newPresence
|
||||
joins[key].metas = joinedMetas
|
||||
}
|
||||
if(leftMetas.length > 0){
|
||||
leaves[key] = this.clone(currentPresence)
|
||||
leaves[key].metas = leftMetas
|
||||
}
|
||||
} else {
|
||||
joins[key] = newPresence
|
||||
}
|
||||
})
|
||||
return this.syncDiff(state, {joins: joins, leaves: leaves}, onJoin, onLeave)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Used to sync a diff of presence join and leave
|
||||
* events from the server, as they happen. Like `syncState`, `syncDiff`
|
||||
* accepts optional `onJoin` and `onLeave` callbacks to react to a user
|
||||
* joining or leaving from a device.
|
||||
*
|
||||
* @param {Record<string, PresenceState>} state
|
||||
* @param {PresenceDiff} diff
|
||||
* @param {PresenceOnJoin} onJoin
|
||||
* @param {PresenceOnLeave} onLeave
|
||||
*
|
||||
* @returns {Record<string, PresenceState>}
|
||||
*/
|
||||
static syncDiff(state, diff, onJoin, onLeave){
|
||||
state = this.toNullProtoObj(state)
|
||||
let {joins, leaves} = this.clone(diff)
|
||||
if(!onJoin){ onJoin = function (){ } }
|
||||
if(!onLeave){ onLeave = function (){ } }
|
||||
|
||||
this.map(joins, (key, newPresence) => {
|
||||
let currentPresence = state[key]
|
||||
state[key] = this.clone(newPresence)
|
||||
if(currentPresence){
|
||||
let joinedRefs = state[key].metas.map(m => m.phx_ref)
|
||||
let curMetas = currentPresence.metas.filter(m => joinedRefs.indexOf(m.phx_ref) < 0)
|
||||
state[key].metas.unshift(...curMetas)
|
||||
}
|
||||
onJoin(key, currentPresence, newPresence)
|
||||
})
|
||||
this.map(leaves, (key, leftPresence) => {
|
||||
let currentPresence = state[key]
|
||||
if(!currentPresence){ return }
|
||||
let refsToRemove = leftPresence.metas.map(m => m.phx_ref)
|
||||
currentPresence.metas = currentPresence.metas.filter(p => {
|
||||
return refsToRemove.indexOf(p.phx_ref) < 0
|
||||
})
|
||||
onLeave(key, currentPresence, leftPresence)
|
||||
if(currentPresence.metas.length === 0){
|
||||
delete state[key]
|
||||
}
|
||||
})
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array of presences, with selected metadata.
|
||||
*
|
||||
* @template [T=PresenceState]
|
||||
* @param {Record<string, PresenceState>} presences
|
||||
* @param {((key: string, obj: PresenceState) => T)} [chooser]
|
||||
*
|
||||
* @returns {T[]}
|
||||
*/
|
||||
static list(presences, chooser){
|
||||
if(!chooser){ chooser = function (key, pres){ return pres } }
|
||||
|
||||
return this.map(presences, (key, presence) => {
|
||||
return chooser(key, presence)
|
||||
})
|
||||
}
|
||||
|
||||
// private
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {Record<string, PresenceState>} obj
|
||||
* @param {(key: string, obj: PresenceState) => T} func
|
||||
*/
|
||||
static map(obj, func){
|
||||
return Object.getOwnPropertyNames(obj).map(key => func(key, obj[key]))
|
||||
}
|
||||
|
||||
// Presence keys are chosen on the server and may collide with
|
||||
// Object.prototype properties ("__proto__", "constructor", ...), so any
|
||||
// object indexed by presence key must not have a prototype chain
|
||||
//
|
||||
// TODO: replace the null-prototype objects with Maps in Phoenix 2.0
|
||||
// (breaking change for the lower-level static API)
|
||||
static toNullProtoObj(obj){
|
||||
if(Object.getPrototypeOf(obj) === null){ return obj }
|
||||
let cleaned = Object.create(null)
|
||||
Object.getOwnPropertyNames(obj).forEach(key => {
|
||||
cleaned[key] = obj[key]
|
||||
})
|
||||
return cleaned
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {T} obj
|
||||
* @returns {T}
|
||||
*/
|
||||
static clone(obj){ return JSON.parse(JSON.stringify(obj)) }
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* @import Channel from "./channel"
|
||||
* @import { ChannelEvent } from "./types"
|
||||
*/
|
||||
export default class Push {
|
||||
/**
|
||||
* Initializes the Push
|
||||
* @param {Channel} channel - The Channel
|
||||
* @param {ChannelEvent} event - The event, for example `"phx_join"`
|
||||
* @param {() => Record<string, unknown>} payload - The payload, for example `{user_id: 123}`
|
||||
* @param {number} timeout - The push timeout in milliseconds
|
||||
*/
|
||||
constructor(channel, event, payload, timeout){
|
||||
/** @type{Channel} */
|
||||
this.channel = channel
|
||||
/** @type{ChannelEvent} */
|
||||
this.event = event
|
||||
/** @type{() => Record<string, unknown>} */
|
||||
this.payload = payload || function (){ return {} }
|
||||
this.receivedResp = null
|
||||
/** @type{number} */
|
||||
this.timeout = timeout
|
||||
/** @type{(ReturnType<typeof setTimeout>) | null} */
|
||||
this.timeoutTimer = null
|
||||
/** @type{{status: string; callback: (response: any) => void}[]} */
|
||||
this.recHooks = []
|
||||
/** @type{boolean} */
|
||||
this.sent = false
|
||||
/** @type{string | null | undefined} */
|
||||
this.ref = undefined
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} timeout
|
||||
*/
|
||||
resend(timeout){
|
||||
this.timeout = timeout
|
||||
this.reset()
|
||||
this.send()
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
send(){
|
||||
if(this.hasReceived("timeout")){ return }
|
||||
this.startTimeout()
|
||||
this.sent = true
|
||||
this.channel.socket.push({
|
||||
topic: this.channel.topic,
|
||||
event: this.event,
|
||||
payload: this.payload(),
|
||||
ref: this.ref,
|
||||
join_ref: this.channel.joinRef()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} status
|
||||
* @param {(response: any) => void} callback
|
||||
*/
|
||||
receive(status, callback){
|
||||
if(this.hasReceived(status)){
|
||||
callback(this.receivedResp.response)
|
||||
}
|
||||
|
||||
this.recHooks.push({status, callback})
|
||||
return this
|
||||
}
|
||||
|
||||
reset(){
|
||||
this.cancelRefEvent()
|
||||
this.ref = null
|
||||
this.refEvent = null
|
||||
this.receivedResp = null
|
||||
this.sent = false
|
||||
}
|
||||
|
||||
destroy(){
|
||||
this.cancelRefEvent()
|
||||
this.cancelTimeout()
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
matchReceive({status, response, _ref}){
|
||||
this.recHooks.filter(h => h.status === status)
|
||||
.forEach(h => h.callback(response))
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
cancelRefEvent(){
|
||||
if(!this.refEvent){ return }
|
||||
this.channel.off(this.refEvent)
|
||||
}
|
||||
|
||||
cancelTimeout(){
|
||||
clearTimeout(this.timeoutTimer)
|
||||
this.timeoutTimer = null
|
||||
}
|
||||
|
||||
startTimeout(){
|
||||
if(this.timeoutTimer){ this.cancelTimeout() }
|
||||
this.ref = this.channel.socket.makeRef()
|
||||
this.refEvent = this.channel.replyEventName(this.ref)
|
||||
|
||||
this.channel.on(this.refEvent, payload => {
|
||||
this.cancelRefEvent()
|
||||
this.cancelTimeout()
|
||||
this.receivedResp = payload
|
||||
this.matchReceive(payload)
|
||||
})
|
||||
|
||||
this.timeoutTimer = setTimeout(() => {
|
||||
this.trigger("timeout", {})
|
||||
}, this.timeout)
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
hasReceived(status){
|
||||
return this.receivedResp && this.receivedResp.status === status
|
||||
}
|
||||
|
||||
trigger(status, response){
|
||||
this.channel.trigger(this.refEvent, {status, response})
|
||||
}
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
/* The default serializer for encoding and decoding messages */
|
||||
import {
|
||||
CHANNEL_EVENTS
|
||||
} from "./constants"
|
||||
|
||||
/**
|
||||
* @import { Message } from "./types"
|
||||
*/
|
||||
|
||||
export default {
|
||||
HEADER_LENGTH: 1,
|
||||
META_LENGTH: 4,
|
||||
KINDS: {push: 0, reply: 1, broadcast: 2},
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {Message<Record<string, any>>} msg
|
||||
* @param {(msg: ArrayBuffer | string) => T} callback
|
||||
* @returns {T}
|
||||
*/
|
||||
encode(msg, callback){
|
||||
if(msg.payload.constructor === ArrayBuffer){
|
||||
return callback(this.binaryEncode(msg))
|
||||
} else {
|
||||
let payload = [msg.join_ref, msg.ref, msg.topic, msg.event, msg.payload]
|
||||
return callback(JSON.stringify(payload))
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {ArrayBuffer | string} rawPayload
|
||||
* @param {(msg: Message<unknown>) => T} callback
|
||||
* @returns {T}
|
||||
*/
|
||||
decode(rawPayload, callback){
|
||||
if(rawPayload.constructor === ArrayBuffer){
|
||||
return callback(this.binaryDecode(rawPayload))
|
||||
} else {
|
||||
let [join_ref, ref, topic, event, payload] = JSON.parse(rawPayload)
|
||||
return callback({join_ref, ref, topic, event, payload})
|
||||
}
|
||||
},
|
||||
|
||||
/** @private */
|
||||
binaryEncode(message){
|
||||
let {join_ref, ref, event, topic, payload} = message
|
||||
let encoder = new TextEncoder()
|
||||
let joinRefBytes = encoder.encode(join_ref)
|
||||
let refBytes = encoder.encode(ref)
|
||||
let topicBytes = encoder.encode(topic)
|
||||
let eventBytes = encoder.encode(event)
|
||||
|
||||
this.assertFieldSize(joinRefBytes.byteLength, "join_ref")
|
||||
this.assertFieldSize(refBytes.byteLength, "ref")
|
||||
this.assertFieldSize(topicBytes.byteLength, "topic")
|
||||
this.assertFieldSize(eventBytes.byteLength, "event")
|
||||
|
||||
let metaLength = this.META_LENGTH + joinRefBytes.byteLength + refBytes.byteLength + topicBytes.byteLength + eventBytes.byteLength
|
||||
let header = new ArrayBuffer(this.HEADER_LENGTH + metaLength)
|
||||
let headerBytes = new Uint8Array(header)
|
||||
let view = new DataView(header)
|
||||
let offset = 0
|
||||
|
||||
view.setUint8(offset++, this.KINDS.push) // kind
|
||||
view.setUint8(offset++, joinRefBytes.byteLength)
|
||||
view.setUint8(offset++, refBytes.byteLength)
|
||||
view.setUint8(offset++, topicBytes.byteLength)
|
||||
view.setUint8(offset++, eventBytes.byteLength)
|
||||
headerBytes.set(joinRefBytes, offset); offset += joinRefBytes.byteLength
|
||||
headerBytes.set(refBytes, offset); offset += refBytes.byteLength
|
||||
headerBytes.set(topicBytes, offset); offset += topicBytes.byteLength
|
||||
headerBytes.set(eventBytes, offset); offset += eventBytes.byteLength
|
||||
|
||||
var combined = new Uint8Array(header.byteLength + payload.byteLength)
|
||||
combined.set(headerBytes, 0)
|
||||
combined.set(new Uint8Array(payload), header.byteLength)
|
||||
|
||||
return combined.buffer
|
||||
},
|
||||
|
||||
assertFieldSize(size, name){
|
||||
if(size > 255){
|
||||
throw new Error(`unable to convert ${name} to binary: must be less than or equal to 255 bytes, but is ${size} bytes`)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
binaryDecode(buffer){
|
||||
let view = new DataView(buffer)
|
||||
let kind = view.getUint8(0)
|
||||
let decoder = new TextDecoder()
|
||||
switch(kind){
|
||||
case this.KINDS.push: return this.decodePush(buffer, view, decoder)
|
||||
case this.KINDS.reply: return this.decodeReply(buffer, view, decoder)
|
||||
case this.KINDS.broadcast: return this.decodeBroadcast(buffer, view, decoder)
|
||||
}
|
||||
},
|
||||
|
||||
/** @private */
|
||||
decodePush(buffer, view, decoder){
|
||||
let joinRefSize = view.getUint8(1)
|
||||
let topicSize = view.getUint8(2)
|
||||
let eventSize = view.getUint8(3)
|
||||
let offset = this.HEADER_LENGTH + this.META_LENGTH - 1 // pushes have no ref
|
||||
let joinRef = decoder.decode(buffer.slice(offset, offset + joinRefSize))
|
||||
offset = offset + joinRefSize
|
||||
let topic = decoder.decode(buffer.slice(offset, offset + topicSize))
|
||||
offset = offset + topicSize
|
||||
let event = decoder.decode(buffer.slice(offset, offset + eventSize))
|
||||
offset = offset + eventSize
|
||||
let data = buffer.slice(offset, buffer.byteLength)
|
||||
return {join_ref: joinRef, ref: null, topic: topic, event: event, payload: data}
|
||||
},
|
||||
|
||||
/** @private */
|
||||
decodeReply(buffer, view, decoder){
|
||||
let joinRefSize = view.getUint8(1)
|
||||
let refSize = view.getUint8(2)
|
||||
let topicSize = view.getUint8(3)
|
||||
let eventSize = view.getUint8(4)
|
||||
let offset = this.HEADER_LENGTH + this.META_LENGTH
|
||||
let joinRef = decoder.decode(buffer.slice(offset, offset + joinRefSize))
|
||||
offset = offset + joinRefSize
|
||||
let ref = decoder.decode(buffer.slice(offset, offset + refSize))
|
||||
offset = offset + refSize
|
||||
let topic = decoder.decode(buffer.slice(offset, offset + topicSize))
|
||||
offset = offset + topicSize
|
||||
let event = decoder.decode(buffer.slice(offset, offset + eventSize))
|
||||
offset = offset + eventSize
|
||||
let data = buffer.slice(offset, buffer.byteLength)
|
||||
let payload = {status: event, response: data}
|
||||
return {join_ref: joinRef, ref: ref, topic: topic, event: CHANNEL_EVENTS.reply, payload: payload}
|
||||
},
|
||||
|
||||
/** @private */
|
||||
decodeBroadcast(buffer, view, decoder){
|
||||
let topicSize = view.getUint8(1)
|
||||
let eventSize = view.getUint8(2)
|
||||
let offset = this.HEADER_LENGTH + 2
|
||||
let topic = decoder.decode(buffer.slice(offset, offset + topicSize))
|
||||
offset = offset + topicSize
|
||||
let event = decoder.decode(buffer.slice(offset, offset + eventSize))
|
||||
offset = offset + eventSize
|
||||
let data = buffer.slice(offset, buffer.byteLength)
|
||||
|
||||
return {join_ref: null, ref: null, topic: topic, event: event, payload: data}
|
||||
}
|
||||
}
|
||||
+756
@@ -0,0 +1,756 @@
|
||||
import {
|
||||
global,
|
||||
phxWindow,
|
||||
CHANNEL_EVENTS,
|
||||
DEFAULT_TIMEOUT,
|
||||
DEFAULT_VSN,
|
||||
SOCKET_STATES,
|
||||
TRANSPORTS,
|
||||
WS_CLOSE_NORMAL,
|
||||
AUTH_TOKEN_PREFIX
|
||||
} from "./constants"
|
||||
|
||||
import {
|
||||
closure
|
||||
} from "./utils"
|
||||
|
||||
import Ajax from "./ajax"
|
||||
import Channel from "./channel"
|
||||
import LongPoll from "./longpoll"
|
||||
import Serializer from "./serializer"
|
||||
import Timer from "./timer"
|
||||
|
||||
/**
|
||||
* @import { Encode, Decode, Message, Vsn, SocketTransport, Params, SocketOnOpen, SocketOnClose, SocketOnError, SocketOnMessage, SocketOptions, SocketStateChangeCallbacks, HeartbeatCallback } from "./types"
|
||||
*/
|
||||
|
||||
export default class Socket {
|
||||
/** Initializes the Socket *
|
||||
*
|
||||
* For IE8 support use an ES5-shim (https://github.com/es-shims/es5-shim)
|
||||
*
|
||||
* @constructor
|
||||
* @param {string} endPoint - The string WebSocket endpoint, ie, `"ws://example.com/socket"`,
|
||||
* `"wss://example.com"`
|
||||
* `"/socket"` (inherited host & protocol)
|
||||
* @param {SocketOptions} [opts] - Optional configuration
|
||||
*/
|
||||
constructor(endPoint, opts = {}){
|
||||
/** @type{SocketStateChangeCallbacks} */
|
||||
this.stateChangeCallbacks = {open: [], close: [], error: [], message: []}
|
||||
/** @type{Channel[]} */
|
||||
this.channels = []
|
||||
/** @type{(() => void)[]} */
|
||||
this.sendBuffer = []
|
||||
/** @type{number} */
|
||||
this.ref = 0
|
||||
/** @type{?string} */
|
||||
this.fallbackRef = null
|
||||
/** @type{number} */
|
||||
this.timeout = opts.timeout || DEFAULT_TIMEOUT
|
||||
/** @type{SocketTransport} */
|
||||
this.transport = opts.transport || global.WebSocket || LongPoll
|
||||
/** @type{InstanceType<SocketTransport> | undefined | null} */
|
||||
this.conn = undefined
|
||||
/** @type{boolean} */
|
||||
this.primaryPassedHealthCheck = false
|
||||
/** @type{number | undefined} */
|
||||
this.longPollFallbackMs = opts.longPollFallbackMs
|
||||
/** @type{ReturnType<typeof setTimeout>} */
|
||||
this.fallbackTimer = null
|
||||
// In some environments (sandboxed iframes without `allow-same-origin`,
|
||||
// in-app webviews, "block third-party storage" privacy modes), reading
|
||||
// `global.sessionStorage` throws SecurityError at the property-access level.
|
||||
// Wrap the read so the Socket constructor cannot throw synchronously.
|
||||
let envSessionStorage = null
|
||||
try { envSessionStorage = global && global.sessionStorage } catch {}
|
||||
/** @type{Storage} */
|
||||
this.sessionStore = opts.sessionStorage || envSessionStorage
|
||||
/** @type{number} */
|
||||
this.establishedConnections = 0
|
||||
/** @type{Encode<void>} */
|
||||
this.defaultEncoder = Serializer.encode.bind(Serializer)
|
||||
/** @type{Decode<void>} */
|
||||
this.defaultDecoder = Serializer.decode.bind(Serializer)
|
||||
// We start with closeWasClean true to avoid the visibility change
|
||||
// logic from connecting if the socket was never connected in the first place.
|
||||
// transportConnect sets it to false on open.
|
||||
/** @type{boolean} */
|
||||
this.closeWasClean = true
|
||||
/** @type{boolean} */
|
||||
this.disconnecting = false
|
||||
/** @type{BinaryType} */
|
||||
this.binaryType = opts.binaryType || "arraybuffer"
|
||||
/** @type{number} */
|
||||
this.connectClock = 1
|
||||
/** @type{boolean} */
|
||||
this.pageHidden = false
|
||||
/** @type{Encode<void>} */
|
||||
this.encode = undefined
|
||||
/** @type{Decode<void>} */
|
||||
this.decode = undefined
|
||||
if(this.transport !== LongPoll){
|
||||
this.encode = opts.encode || this.defaultEncoder
|
||||
this.decode = opts.decode || this.defaultDecoder
|
||||
} else {
|
||||
this.encode = this.defaultEncoder
|
||||
this.decode = this.defaultDecoder
|
||||
}
|
||||
/** @type{number | null} */
|
||||
let awaitingConnectionOnPageShow = null
|
||||
if(phxWindow && phxWindow.addEventListener){
|
||||
phxWindow.addEventListener("pagehide", _e => {
|
||||
if(this.conn){
|
||||
this.disconnect()
|
||||
awaitingConnectionOnPageShow = this.connectClock
|
||||
}
|
||||
})
|
||||
phxWindow.addEventListener("pageshow", _e => {
|
||||
if(awaitingConnectionOnPageShow === this.connectClock){
|
||||
awaitingConnectionOnPageShow = null
|
||||
this.connect()
|
||||
}
|
||||
})
|
||||
phxWindow.addEventListener("visibilitychange", () => {
|
||||
if(document.visibilityState === "hidden"){
|
||||
this.pageHidden = true
|
||||
} else {
|
||||
this.pageHidden = false
|
||||
// reconnect immediately
|
||||
if(!this.isConnected() && !this.closeWasClean){
|
||||
this.teardown(() => this.connect())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
/** @type{number} */
|
||||
this.heartbeatIntervalMs = opts.heartbeatIntervalMs || 30000
|
||||
/** @type{boolean} */
|
||||
this.autoSendHeartbeat = opts.autoSendHeartbeat ?? true
|
||||
/** @type{HeartbeatCallback} */
|
||||
this.heartbeatCallback = opts.heartbeatCallback ?? (() => {})
|
||||
/** @type{(tries: number) => number} */
|
||||
this.rejoinAfterMs = (tries) => {
|
||||
if(opts.rejoinAfterMs){
|
||||
return opts.rejoinAfterMs(tries)
|
||||
} else {
|
||||
return [1000, 2000, 5000][tries - 1] || 10000
|
||||
}
|
||||
}
|
||||
/** @type{(tries: number) => number} */
|
||||
this.reconnectAfterMs = (tries) => {
|
||||
if(opts.reconnectAfterMs){
|
||||
return opts.reconnectAfterMs(tries)
|
||||
} else {
|
||||
return [10, 50, 100, 150, 200, 250, 500, 1000, 2000][tries - 1] || 5000
|
||||
}
|
||||
}
|
||||
/** @type{((kind: string, msg: string, data: any) => void) | null} */
|
||||
this.logger = opts.logger || null
|
||||
if(!this.logger && opts.debug){
|
||||
this.logger = (kind, msg, data) => { console.log(`${kind}: ${msg}`, data) }
|
||||
}
|
||||
/** @type{number} */
|
||||
this.longpollerTimeout = opts.longpollerTimeout || 20000
|
||||
/** @type{() => Params} */
|
||||
this.params = closure(opts.params || {})
|
||||
/** @type{string} */
|
||||
this.endPoint = `${endPoint}/${TRANSPORTS.websocket}`
|
||||
/** @type{Vsn} */
|
||||
this.vsn = opts.vsn || DEFAULT_VSN
|
||||
/** @type{ReturnType<typeof setTimeout>} */
|
||||
this.heartbeatTimeoutTimer = null
|
||||
/** @type{ReturnType<typeof setTimeout>} */
|
||||
this.heartbeatTimer = null
|
||||
/** @type{number | null} */
|
||||
this.heartbeatSentAt = null
|
||||
/** @type{?string} */
|
||||
this.pendingHeartbeatRef = null
|
||||
/** @type{Timer} */
|
||||
this.reconnectTimer = new Timer( () => {
|
||||
if(this.pageHidden){
|
||||
this.log("Not reconnecting as page is hidden!")
|
||||
this.teardown()
|
||||
return
|
||||
}
|
||||
|
||||
this.teardown(async () => {
|
||||
if(opts.beforeReconnect) await opts.beforeReconnect()
|
||||
this.connect()
|
||||
})
|
||||
}, this.reconnectAfterMs)
|
||||
/** @type{(() => string) | undefined} */
|
||||
this.authToken = opts.authToken && closure(opts.authToken)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the LongPoll transport reference
|
||||
*/
|
||||
getLongPollTransport(){ return LongPoll }
|
||||
|
||||
/**
|
||||
* Disconnects and replaces the active transport
|
||||
*
|
||||
* @param {SocketTransport} newTransport - The new transport class to instantiate
|
||||
*
|
||||
*/
|
||||
replaceTransport(newTransport){
|
||||
this.connectClock++
|
||||
this.closeWasClean = true
|
||||
clearTimeout(this.fallbackTimer)
|
||||
this.reconnectTimer.reset()
|
||||
if(this.conn){
|
||||
this.conn.close()
|
||||
this.conn = null
|
||||
}
|
||||
this.transport = newTransport
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the socket protocol
|
||||
*
|
||||
* @returns {"wss" | "ws"}
|
||||
*/
|
||||
protocol(){ return location.protocol.match(/^https/) ? "wss" : "ws" }
|
||||
|
||||
/**
|
||||
* The fully qualified socket url
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
endPointURL(){
|
||||
let uri = Ajax.appendParams(
|
||||
Ajax.appendParams(this.endPoint, this.params()), {vsn: this.vsn})
|
||||
if(uri.charAt(0) !== "/"){ return uri }
|
||||
if(uri.charAt(1) === "/"){ return `${this.protocol()}:${uri}` }
|
||||
|
||||
return `${this.protocol()}://${location.host}${uri}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the socket
|
||||
*
|
||||
* See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes for valid status codes.
|
||||
*
|
||||
* @param {() => void} [callback] - Optional callback which is called after socket is disconnected.
|
||||
* @param {number} [code] - A status code for disconnection (Optional).
|
||||
* @param {string} [reason] - A textual description of the reason to disconnect. (Optional)
|
||||
*/
|
||||
disconnect(callback, code, reason){
|
||||
this.connectClock++
|
||||
this.disconnecting = true
|
||||
this.closeWasClean = true
|
||||
clearTimeout(this.fallbackTimer)
|
||||
this.reconnectTimer.reset()
|
||||
this.teardown(() => {
|
||||
this.disconnecting = false
|
||||
callback && callback()
|
||||
}, code, reason)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Params} [params] - [DEPRECATED] The params to send when connecting, for example `{user_id: userToken}`
|
||||
*
|
||||
* Passing params to connect is deprecated; pass them in the Socket constructor instead:
|
||||
* `new Socket("/socket", {params: {user_id: userToken}})`.
|
||||
*/
|
||||
connect(params){
|
||||
if(params){
|
||||
console && console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor")
|
||||
this.params = closure(params)
|
||||
}
|
||||
if(this.conn && !this.disconnecting){ return }
|
||||
if(this.longPollFallbackMs && this.transport !== LongPoll){
|
||||
this.connectWithFallback(LongPoll, this.longPollFallbackMs)
|
||||
} else {
|
||||
this.transportConnect()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the message. Override `this.logger` for specialized logging. noops by default
|
||||
* @param {string} kind
|
||||
* @param {string} msg
|
||||
* @param {Object} data
|
||||
*/
|
||||
log(kind, msg, data){ this.logger && this.logger(kind, msg, data) }
|
||||
|
||||
/**
|
||||
* Returns true if a logger has been set on this socket.
|
||||
*/
|
||||
hasLogger(){ return this.logger !== null }
|
||||
|
||||
/**
|
||||
* Registers callbacks for connection open events
|
||||
*
|
||||
* @example socket.onOpen(function(){ console.info("the socket was opened") })
|
||||
*
|
||||
* @param {SocketOnOpen} callback
|
||||
*/
|
||||
onOpen(callback){
|
||||
let ref = this.makeRef()
|
||||
this.stateChangeCallbacks.open.push([ref, callback])
|
||||
return ref
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers callbacks for connection close events
|
||||
* @param {SocketOnClose} callback
|
||||
* @returns {string}
|
||||
*/
|
||||
onClose(callback){
|
||||
let ref = this.makeRef()
|
||||
this.stateChangeCallbacks.close.push([ref, callback])
|
||||
return ref
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers callbacks for connection error events
|
||||
*
|
||||
* @example socket.onError(function(error){ alert("An error occurred") })
|
||||
*
|
||||
* @param {SocketOnError} callback
|
||||
* @returns {string}
|
||||
*/
|
||||
onError(callback){
|
||||
let ref = this.makeRef()
|
||||
this.stateChangeCallbacks.error.push([ref, callback])
|
||||
return ref
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers callbacks for connection message events
|
||||
* @param {SocketOnMessage} callback
|
||||
* @returns {string}
|
||||
*/
|
||||
onMessage(callback){
|
||||
let ref = this.makeRef()
|
||||
this.stateChangeCallbacks.message.push([ref, callback])
|
||||
return ref
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a callback that receives lifecycle events for internal heartbeat messages.
|
||||
* Useful for instrumenting connection health (e.g. sent/ok/timeout/disconnected).
|
||||
* @param {HeartbeatCallback} callback
|
||||
*/
|
||||
onHeartbeat(callback){
|
||||
this.heartbeatCallback = callback
|
||||
}
|
||||
|
||||
/**
|
||||
* Pings the server and invokes the callback with the RTT in milliseconds
|
||||
* @param {(timeDelta: number) => void} callback
|
||||
*
|
||||
* Returns true if the ping was pushed or false if unable to be pushed.
|
||||
*/
|
||||
ping(callback){
|
||||
if(!this.isConnected()){ return false }
|
||||
let ref = this.makeRef()
|
||||
let startTime = Date.now()
|
||||
this.push({topic: "phoenix", event: "heartbeat", payload: {}, ref: ref})
|
||||
let onMsgRef = this.onMessage(msg => {
|
||||
if(msg.ref === ref){
|
||||
this.off([onMsgRef])
|
||||
callback(Date.now() - startTime)
|
||||
}
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {Function}
|
||||
*/
|
||||
transportName(transport){
|
||||
// JavaScript minification, enabled by default in production in Phoenix
|
||||
// projects, renames symbols to reduce code size.
|
||||
// See https://esbuild.github.io/api/#keep-names.
|
||||
// This helper ensures we return the correct name for the LongPoll transport
|
||||
// even after minification. The other common transport is WebSocket, which
|
||||
// is native to browsers and does not need special handling.
|
||||
switch(transport){
|
||||
case LongPoll: return "LongPoll"
|
||||
default: return transport.name
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
transportConnect(){
|
||||
this.connectClock++
|
||||
this.closeWasClean = false
|
||||
let protocols = undefined
|
||||
// Sec-WebSocket-Protocol based token
|
||||
// (longpoll uses Authorization header instead)
|
||||
if(this.authToken){
|
||||
protocols = ["phoenix", `${AUTH_TOKEN_PREFIX}${btoa(this.authToken()).replace(/=/g, "")}`]
|
||||
}
|
||||
this.conn = new this.transport(this.endPointURL(), protocols)
|
||||
this.conn.binaryType = this.binaryType
|
||||
this.conn.timeout = this.longpollerTimeout
|
||||
this.conn.onopen = () => this.onConnOpen()
|
||||
this.conn.onerror = error => this.onConnError(error)
|
||||
this.conn.onmessage = event => this.onConnMessage(event)
|
||||
this.conn.onclose = event => this.onConnClose(event)
|
||||
}
|
||||
|
||||
getSession(key){ return this.sessionStore && this.sessionStore.getItem(key) }
|
||||
|
||||
storeSession(key, val){ this.sessionStore && this.sessionStore.setItem(key, val) }
|
||||
|
||||
connectWithFallback(fallbackTransport, fallbackThreshold = 2500){
|
||||
clearTimeout(this.fallbackTimer)
|
||||
let established = false
|
||||
let primaryTransport = true
|
||||
let openRef, errorRef
|
||||
let fallbackTransportName = this.transportName(fallbackTransport)
|
||||
let fallback = (reason) => {
|
||||
this.log("transport", `falling back to ${fallbackTransportName}...`, reason)
|
||||
this.off([openRef, errorRef])
|
||||
primaryTransport = false
|
||||
this.replaceTransport(fallbackTransport)
|
||||
this.transportConnect()
|
||||
}
|
||||
if(this.getSession(`phx:fallback:${fallbackTransportName}`)){ return fallback("memorized") }
|
||||
|
||||
this.fallbackTimer = setTimeout(fallback, fallbackThreshold)
|
||||
|
||||
errorRef = this.onError(reason => {
|
||||
this.log("transport", "error", reason)
|
||||
if(primaryTransport && !established){
|
||||
clearTimeout(this.fallbackTimer)
|
||||
fallback(reason)
|
||||
}
|
||||
})
|
||||
if(this.fallbackRef){
|
||||
this.off([this.fallbackRef])
|
||||
}
|
||||
this.fallbackRef = this.onOpen(() => {
|
||||
established = true
|
||||
if(!primaryTransport){
|
||||
let fallbackTransportName = this.transportName(fallbackTransport)
|
||||
// only memorize LP if we never connected to primary
|
||||
if(!this.primaryPassedHealthCheck){ this.storeSession(`phx:fallback:${fallbackTransportName}`, "true") }
|
||||
return this.log("transport", `established ${fallbackTransportName} fallback`)
|
||||
}
|
||||
// if we've established primary, give the fallback a new period to attempt ping
|
||||
clearTimeout(this.fallbackTimer)
|
||||
this.fallbackTimer = setTimeout(fallback, fallbackThreshold)
|
||||
this.ping(rtt => {
|
||||
this.log("transport", "connected to primary after", rtt)
|
||||
this.primaryPassedHealthCheck = true
|
||||
clearTimeout(this.fallbackTimer)
|
||||
})
|
||||
})
|
||||
this.transportConnect()
|
||||
}
|
||||
|
||||
clearHeartbeats(){
|
||||
clearTimeout(this.heartbeatTimer)
|
||||
clearTimeout(this.heartbeatTimeoutTimer)
|
||||
}
|
||||
|
||||
onConnOpen(){
|
||||
if(this.hasLogger()) this.log("transport", `connected to ${this.endPointURL()}`)
|
||||
this.closeWasClean = false
|
||||
this.disconnecting = false
|
||||
this.establishedConnections++
|
||||
this.flushSendBuffer()
|
||||
this.reconnectTimer.reset()
|
||||
if(this.autoSendHeartbeat){
|
||||
this.resetHeartbeat()
|
||||
}
|
||||
this.triggerStateCallbacks("open")
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
|
||||
heartbeatTimeout(){
|
||||
if(this.pendingHeartbeatRef){
|
||||
this.pendingHeartbeatRef = null
|
||||
this.heartbeatSentAt = null
|
||||
if(this.hasLogger()){ this.log("transport", "heartbeat timeout. Attempting to re-establish connection") }
|
||||
try {
|
||||
this.heartbeatCallback("timeout")
|
||||
} catch (e){
|
||||
this.log("error", "error in heartbeat callback", e)
|
||||
}
|
||||
this.triggerChanError(new Error("heartbeat timeout"))
|
||||
this.closeWasClean = false
|
||||
this.teardown(() => this.reconnectTimer.scheduleTimeout(), WS_CLOSE_NORMAL, "heartbeat timeout")
|
||||
}
|
||||
}
|
||||
|
||||
resetHeartbeat(){
|
||||
if(this.conn && this.conn.skipHeartbeat){ return }
|
||||
this.pendingHeartbeatRef = null
|
||||
this.clearHeartbeats()
|
||||
this.heartbeatTimer = setTimeout(() => this.sendHeartbeat(), this.heartbeatIntervalMs)
|
||||
}
|
||||
|
||||
teardown(callback, code, reason){
|
||||
if(!this.conn){
|
||||
return callback && callback()
|
||||
}
|
||||
|
||||
// If someone calls connect before we finish tearing down,
|
||||
// we create a new connection, but we still want to finish tearing down the old one.
|
||||
const connToClose = this.conn
|
||||
|
||||
this.waitForBufferDone(connToClose, () => {
|
||||
if(code){ connToClose.close(code, reason || "") } else { connToClose.close() }
|
||||
|
||||
this.waitForSocketClosed(connToClose, () => {
|
||||
if(this.conn === connToClose){
|
||||
this.conn.onopen = function (){ } // noop
|
||||
this.conn.onerror = function (){ } // noop
|
||||
this.conn.onmessage = function (){ } // noop
|
||||
this.conn.onclose = function (){ } // noop
|
||||
this.conn = null
|
||||
}
|
||||
|
||||
callback && callback()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
waitForBufferDone(conn, callback, tries = 1){
|
||||
if(tries === 5 || !conn.bufferedAmount){
|
||||
callback()
|
||||
return
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.waitForBufferDone(conn, callback, tries + 1)
|
||||
}, 150 * tries)
|
||||
}
|
||||
|
||||
waitForSocketClosed(conn, callback, tries = 1){
|
||||
if(tries === 5 || conn.readyState === SOCKET_STATES.closed){
|
||||
callback()
|
||||
return
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.waitForSocketClosed(conn, callback, tries + 1)
|
||||
}, 150 * tries)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CloseEvent} event
|
||||
*/
|
||||
onConnClose(event){
|
||||
if(this.conn) this.conn.onclose = () => {} // noop to prevent recursive calls in teardown
|
||||
if(this.hasLogger()) this.log("transport", "close", event)
|
||||
this.triggerChanError(event)
|
||||
this.clearHeartbeats()
|
||||
if(!this.closeWasClean){
|
||||
this.reconnectTimer.scheduleTimeout()
|
||||
}
|
||||
this.triggerStateCallbacks("close", event)
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} error
|
||||
*/
|
||||
onConnError(error){
|
||||
if(this.hasLogger()) this.log("transport", "error", error)
|
||||
let transportBefore = this.transport
|
||||
let establishedBefore = this.establishedConnections
|
||||
this.triggerStateCallbacks("error", error, transportBefore, establishedBefore)
|
||||
if(transportBefore === this.transport || establishedBefore > 0){
|
||||
this.triggerChanError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {unknown} [reason] underlying close/error event forwarded to channel error listeners
|
||||
*/
|
||||
triggerChanError(reason){
|
||||
this.channels.forEach(channel => {
|
||||
if(!(channel.isErrored() || channel.isLeaving() || channel.isClosed())){
|
||||
channel.trigger(CHANNEL_EVENTS.error, reason)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
connectionState(){
|
||||
switch(this.conn && this.conn.readyState){
|
||||
case SOCKET_STATES.connecting: return "connecting"
|
||||
case SOCKET_STATES.open: return "open"
|
||||
case SOCKET_STATES.closing: return "closing"
|
||||
default: return "closed"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isConnected(){ return this.connectionState() === "open" }
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Channel} channel
|
||||
*/
|
||||
remove(channel){
|
||||
this.off(channel.stateChangeRefs)
|
||||
this.channels = this.channels.filter(c => c !== channel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes `onOpen`, `onClose`, `onError,` and `onMessage` registrations.
|
||||
*
|
||||
* @param {string[]} refs - list of refs returned by calls to
|
||||
* `onOpen`, `onClose`, `onError,` and `onMessage`
|
||||
*/
|
||||
off(refs){
|
||||
for(let key in this.stateChangeCallbacks){
|
||||
this.stateChangeCallbacks[key] = this.stateChangeCallbacks[key].filter(([ref]) => {
|
||||
return refs.indexOf(ref) === -1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a new channel for the given topic
|
||||
*
|
||||
* @param {string} topic
|
||||
* @param {Params | (() => Params)} [chanParams]- Parameters for the channel
|
||||
* @returns {Channel}
|
||||
*/
|
||||
channel(topic, chanParams = {}){
|
||||
let chan = new Channel(topic, chanParams, this)
|
||||
this.channels.push(chan)
|
||||
return chan
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Message<Record<string, any>>} data
|
||||
*/
|
||||
push(data){
|
||||
if(this.hasLogger()){
|
||||
let {topic, event, payload, ref, join_ref} = data
|
||||
this.log("push", `${topic} ${event} (${join_ref}, ${ref})`, payload)
|
||||
}
|
||||
|
||||
if(this.isConnected()){
|
||||
this.encode(data, result => this.conn.send(result))
|
||||
} else {
|
||||
this.sendBuffer.push(() => this.encode(data, result => this.conn.send(result)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the next message ref, accounting for overflows
|
||||
* @returns {string}
|
||||
*/
|
||||
makeRef(){
|
||||
let newRef = this.ref + 1
|
||||
if(newRef === this.ref){ this.ref = 0 } else { this.ref = newRef }
|
||||
|
||||
return this.ref.toString()
|
||||
}
|
||||
|
||||
sendHeartbeat(){
|
||||
if(!this.isConnected()){
|
||||
try {
|
||||
this.heartbeatCallback("disconnected")
|
||||
} catch (e){
|
||||
this.log("error", "error in heartbeat callback", e)
|
||||
}
|
||||
return
|
||||
}
|
||||
if(this.pendingHeartbeatRef){
|
||||
this.heartbeatTimeout()
|
||||
return
|
||||
}
|
||||
this.pendingHeartbeatRef = this.makeRef()
|
||||
this.heartbeatSentAt = Date.now()
|
||||
this.push({topic: "phoenix", event: "heartbeat", payload: {}, ref: this.pendingHeartbeatRef})
|
||||
try {
|
||||
this.heartbeatCallback("sent")
|
||||
} catch (e){
|
||||
this.log("error", "error in heartbeat callback", e)
|
||||
}
|
||||
this.heartbeatTimeoutTimer = setTimeout(() => this.heartbeatTimeout(), this.heartbeatIntervalMs)
|
||||
}
|
||||
|
||||
flushSendBuffer(){
|
||||
if(this.isConnected() && this.sendBuffer.length > 0){
|
||||
this.sendBuffer.forEach(callback => callback())
|
||||
this.sendBuffer = []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MessageEvent<any>} rawMessage
|
||||
*/
|
||||
onConnMessage(rawMessage){
|
||||
this.decode(rawMessage.data, msg => {
|
||||
let {topic, event, payload, ref, join_ref} = msg
|
||||
if(ref && ref === this.pendingHeartbeatRef){
|
||||
const latency = this.heartbeatSentAt ? Date.now() - this.heartbeatSentAt : undefined
|
||||
this.clearHeartbeats()
|
||||
try {
|
||||
this.heartbeatCallback(payload.status === "ok" ? "ok" : "error", latency)
|
||||
} catch (e){
|
||||
this.log("error", "error in heartbeat callback", e)
|
||||
}
|
||||
this.pendingHeartbeatRef = null
|
||||
this.heartbeatSentAt = null
|
||||
if(this.autoSendHeartbeat){
|
||||
this.heartbeatTimer = setTimeout(() => this.sendHeartbeat(), this.heartbeatIntervalMs)
|
||||
}
|
||||
}
|
||||
|
||||
if(this.hasLogger()) this.log("receive", `${payload.status || ""} ${topic} ${event} ${ref && "(" + ref + ")" || ""}`.trim(), payload)
|
||||
|
||||
for(let i = 0; i < this.channels.length; i++){
|
||||
const channel = this.channels[i]
|
||||
if(!channel.isMember(topic, event, payload, join_ref)){ continue }
|
||||
channel.trigger(event, payload, ref, join_ref)
|
||||
}
|
||||
|
||||
this.triggerStateCallbacks("message", msg)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @template {keyof SocketStateChangeCallbacks} K
|
||||
* @param {K} event
|
||||
* @param {...Parameters<SocketStateChangeCallbacks[K][number][1]>} args
|
||||
* @returns {void}
|
||||
*/
|
||||
triggerStateCallbacks(event, ...args){
|
||||
try {
|
||||
this.stateChangeCallbacks[event].forEach(([_, callback]) => {
|
||||
try {
|
||||
callback(...args)
|
||||
} catch (e){
|
||||
this.log("error", `error in ${event} callback`, e)
|
||||
}
|
||||
})
|
||||
} catch (e){
|
||||
this.log("error", `error triggering ${event} callbacks`, e)
|
||||
}
|
||||
}
|
||||
|
||||
leaveOpenTopic(topic){
|
||||
let dupChannel = this.channels.find(c => c.topic === topic && (c.isJoined() || c.isJoining()))
|
||||
if(dupChannel){
|
||||
if(this.hasLogger()) this.log("transport", `leaving duplicate topic "${topic}"`)
|
||||
dupChannel.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
*
|
||||
* Creates a timer that accepts a `timerCalc` function to perform
|
||||
* calculated timeout retries, such as exponential backoff.
|
||||
*
|
||||
* @example
|
||||
* let reconnectTimer = new Timer(() => this.connect(), function(tries){
|
||||
* return [1000, 5000, 10000][tries - 1] || 10000
|
||||
* })
|
||||
* reconnectTimer.scheduleTimeout() // fires after 1000
|
||||
* reconnectTimer.scheduleTimeout() // fires after 5000
|
||||
* reconnectTimer.reset()
|
||||
* reconnectTimer.scheduleTimeout() // fires after 1000
|
||||
*
|
||||
*/
|
||||
export default class Timer {
|
||||
/**
|
||||
* @param {() => void} callback
|
||||
* @param {(tries: number) => number} timerCalc
|
||||
*/
|
||||
constructor(callback, timerCalc){
|
||||
/** @type {() => void} */
|
||||
this.callback = callback
|
||||
/** @type {(tries: number) => number} */
|
||||
this.timerCalc = timerCalc
|
||||
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
||||
this.timer = undefined
|
||||
/** @type {number} */
|
||||
this.tries = 0
|
||||
}
|
||||
|
||||
reset(){
|
||||
this.tries = 0
|
||||
clearTimeout(this.timer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels any previous scheduleTimeout and schedules callback
|
||||
*/
|
||||
scheduleTimeout(){
|
||||
clearTimeout(this.timer)
|
||||
|
||||
this.timer = setTimeout(() => {
|
||||
this.tries = this.tries + 1
|
||||
this.callback()
|
||||
}, this.timerCalc(this.tries + 1))
|
||||
}
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* @import LongPoll from "./longpoll"
|
||||
*/
|
||||
|
||||
/**
|
||||
* MISC
|
||||
* @typedef {Record<string, unknown>} Params
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {T | (() => T)} Closure
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* CHANNEL
|
||||
* @typedef {(payload: unknown, ref: string | null | undefined, joinRef: string) => void} ChannelBindingCallback
|
||||
* @typedef {(reason: unknown) => void} ChannelOnErrorCallback
|
||||
* @typedef {({event: string, ref: number, callback: ChannelBindingCallback})} ChannelBinding
|
||||
* @typedef {(event: string, payload?: unknown, ref?: ?string, joinRef?: ?string) => unknown} ChannelOnMessage
|
||||
* @typedef {(binding: ChannelBinding, payload: unknown, ref?: ?string) => boolean} ChannelFilterBindings
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* CONSTANTS
|
||||
* @import {SOCKET_STATES, CHANNEL_STATES, CHANNEL_EVENTS, TRANSPORTS, XHR_STATES} from "./constants"
|
||||
* @typedef {"1.0.0" | "2.0.0"} Vsn
|
||||
* @typedef {typeof SOCKET_STATES[keyof typeof SOCKET_STATES]} SocketState
|
||||
* @typedef {typeof CHANNEL_STATES[keyof typeof CHANNEL_STATES]} ChannelState
|
||||
* @typedef {typeof CHANNEL_EVENTS[keyof typeof CHANNEL_EVENTS]} ChannelEvent
|
||||
* @typedef {typeof TRANSPORTS[keyof typeof TRANSPORTS]} Transport
|
||||
* @typedef {typeof XHR_STATES[keyof typeof XHR_STATES]} XhrState
|
||||
*/
|
||||
|
||||
/**
|
||||
* PRESENCE
|
||||
* @typedef {{state: string, diff: string}} PresenceEvents
|
||||
* @typedef {(key: string, currentPresence: PresenceState, newPresence: PresenceState) => void} PresenceOnJoin
|
||||
* @typedef {(key: string, currentPresence: PresenceState, leftPresence: PresenceState) => void} PresenceOnLeave
|
||||
* @typedef {() => void} PresenceOnSync
|
||||
* @typedef {({joins: PresenceState, leaves: PresenceState})} PresenceDiff
|
||||
* @typedef {(
|
||||
* {
|
||||
* metas: {
|
||||
* phx_ref?: string
|
||||
* phx_ref_prev?: string
|
||||
* [key: string]: any
|
||||
* }[]
|
||||
* }
|
||||
*)} PresenceState
|
||||
*
|
||||
* @typedef {Object} PresenceOptions
|
||||
* @property {PresenceEvents} [events]
|
||||
*/
|
||||
|
||||
/**
|
||||
* SERIALIZER
|
||||
* @template T
|
||||
* @typedef {({
|
||||
* join_ref?: string | null;
|
||||
* ref?: string | null;
|
||||
* event: string;
|
||||
* topic: string;
|
||||
* payload: T;
|
||||
* })} Message
|
||||
*/
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {(msg: Message<Record<string, any>>, callback: (result: ArrayBuffer | string) => T) => T} Encode
|
||||
*/
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {(rawPayload: ArrayBuffer | string, callback: (msg: Message<unknown>) => T) => T} Decode
|
||||
*/
|
||||
|
||||
/**
|
||||
* SOCKET
|
||||
* @typedef {(typeof WebSocket | typeof LongPoll)} SocketTransport
|
||||
* @typedef {() => void} SocketOnOpen
|
||||
* @typedef {(event: CloseEvent) => void} SocketOnClose
|
||||
* @typedef {(error: Event, transportBefore: SocketTransport, establishedBefore: number) => void} SocketOnError
|
||||
* @typedef {(rawMessage: Message<unknown>) => void} SocketOnMessage
|
||||
* @typedef {({
|
||||
* open: [string, SocketOnOpen][]
|
||||
* close: [string, SocketOnClose][]
|
||||
* error: [string, SocketOnError][]
|
||||
* message: [string, SocketOnMessage][]
|
||||
* })} SocketStateChangeCallbacks
|
||||
* @typedef {'sent' | 'ok' | 'error' | 'timeout' | 'disconnected'} HeartbeatStatus
|
||||
* @typedef {(status: HeartbeatStatus, latency?: number) => void} HeartbeatCallback
|
||||
*
|
||||
*
|
||||
*
|
||||
* @typedef {Object} SocketOptions
|
||||
* @property {SocketTransport} [transport] - The Websocket Transport, for example WebSocket or Phoenix.LongPoll.
|
||||
*
|
||||
* @property {number} [longPollFallbackMs] - The millisecond time to attempt the primary transport
|
||||
* before falling back to the LongPoll transport. Disabled by default.
|
||||
*
|
||||
* @property {number} [longpollerTimeout] - The millisecond time before LongPoll transport times out. Default 20000.
|
||||
*
|
||||
* @property {boolean} [debug] - When true, enables debug logging. Default false.
|
||||
*
|
||||
* @property {Encode<void>} [encode] - The function to encode outgoing messages.
|
||||
* Defaults to JSON encoder.
|
||||
*
|
||||
* @property {Decode<void>} [decode] - The function to decode incoming messages.
|
||||
* Defaults to JSON:
|
||||
*
|
||||
* ```javascript
|
||||
* (payload, callback) => callback(JSON.parse(payload))
|
||||
* ```
|
||||
*
|
||||
* @property {number} [timeout] - The default timeout in milliseconds to trigger push timeouts.
|
||||
* Defaults `DEFAULT_TIMEOUT`
|
||||
*
|
||||
* @property {number} [heartbeatIntervalMs] - The millisec interval to send a heartbeat message
|
||||
*
|
||||
* @property {boolean} [autoSendHeartbeat] - Whether to automatically send heartbeats after
|
||||
* connection is established.
|
||||
*
|
||||
* Defaults to true.
|
||||
*
|
||||
* @property {HeartbeatCallback} [heartbeatCallback] - The optional function to handle heartbeat status and latency.
|
||||
*
|
||||
* @property {(tries: number) => number} [reconnectAfterMs] - The optional function that returns the
|
||||
* socket reconnect interval, in milliseconds.
|
||||
*
|
||||
* Defaults to stepped backoff of:
|
||||
*
|
||||
* ```javascript
|
||||
* function(tries){
|
||||
* return [10, 50, 100, 150, 200, 250, 500, 1000, 2000][tries - 1] || 5000
|
||||
* }
|
||||
* ````
|
||||
*
|
||||
* @property {(tries: number) => number} [rejoinAfterMs] - The optional function that returns the millisec
|
||||
* rejoin interval for individual channels.
|
||||
*
|
||||
* ```javascript
|
||||
* function(tries){
|
||||
* return [1000, 2000, 5000][tries - 1] || 10000
|
||||
* }
|
||||
* ````
|
||||
*
|
||||
* @property {(kind: string, msg: string, data: any) => void} [logger] - The optional function for specialized logging, ie:
|
||||
*
|
||||
* ```javascript
|
||||
* function(kind, msg, data) {
|
||||
* console.log(`${kind}: ${msg}`, data)
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @property {Closure<Params>} [params] - The optional params to pass when connecting
|
||||
*
|
||||
* @property {Closure<string>} [authToken] - the optional authentication token to be exposed on the server
|
||||
* under the `:auth_token` connect_info key. Can be a string or a function that returns a string.
|
||||
*
|
||||
* @property {BinaryType} [binaryType] - The binary type to use for binary WebSocket frames.
|
||||
*
|
||||
* Defaults to "arraybuffer"
|
||||
*
|
||||
* @property {Vsn} [vsn] - The serializer's protocol version to send on connect.
|
||||
*
|
||||
* Defaults to DEFAULT_VSN.
|
||||
*
|
||||
* @property {Storage} [sessionStorage] - An optional Storage compatible object
|
||||
* Phoenix uses sessionStorage for longpoll fallback history. Overriding the store is
|
||||
* useful when Phoenix won't have access to `sessionStorage`. For example, This could
|
||||
* happen if a site loads a cross-domain channel in an iframe. Example usage:
|
||||
*
|
||||
* class InMemoryStorage {
|
||||
* constructor() { this.storage = {} }
|
||||
* getItem(keyName) { return this.storage[keyName] || null }
|
||||
* removeItem(keyName) { delete this.storage[keyName] }
|
||||
* setItem(keyName, keyValue) { this.storage[keyName] = keyValue }
|
||||
* }
|
||||
*
|
||||
* @property {() => Promise<void>} [beforeReconnect] - Callback ran before socket tries to reconnect.
|
||||
*
|
||||
*/
|
||||
export {}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
*
|
||||
* Wraps value in closure or returns closure
|
||||
*
|
||||
* @template T
|
||||
* @param {T | (() => T)} value
|
||||
* @returns {() => T}
|
||||
*/
|
||||
export let closure = (value) => {
|
||||
if(typeof value === "function"){
|
||||
return /** @type {() => T} */ (value)
|
||||
} else {
|
||||
let closure = function (){ return value }
|
||||
return closure
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "@supabase/phoenix",
|
||||
"version": "0.4.5",
|
||||
"description": "The official JavaScript client for the Phoenix web framework.",
|
||||
"license": "MIT",
|
||||
"module": "./priv/static/phoenix.mjs",
|
||||
"main": "./priv/static/phoenix.cjs.js",
|
||||
"types": "./priv/static/types/index.d.ts",
|
||||
"unpkg": "./priv/static/phoenix.min.js",
|
||||
"jsdelivr": "./priv/static/phoenix.min.js",
|
||||
"exports": {
|
||||
"import": {
|
||||
"types": "./priv/static/types/index.d.ts",
|
||||
"default": "./priv/static/phoenix.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./priv/static/types/index.d.ts",
|
||||
"default": "./priv/static/phoenix.cjs.js"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/supabase/phoenix.git"
|
||||
},
|
||||
"author": "Chris McCord <chris@chrismccord.com> (https://www.phoenixframework.org)",
|
||||
"files": [
|
||||
"README.md",
|
||||
"LICENSE.md",
|
||||
"package.json",
|
||||
"tsconfig.json",
|
||||
"priv/static/*",
|
||||
"assets/js/phoenix/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@stylistic/eslint-plugin": "^5.0.0",
|
||||
"documentation": "^14.0.3",
|
||||
"eslint": "10.6.0",
|
||||
"eslint-plugin-jest": "29.15.4",
|
||||
"jest": "^30.0.0",
|
||||
"jest-environment-jsdom": "^30.0.0",
|
||||
"jsdom": "^29.0.1",
|
||||
"mock-socket": "^9.3.1",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js",
|
||||
"test.coverage": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --coverage",
|
||||
"test.watch": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --watch",
|
||||
"build.types": "tsc",
|
||||
"docs": "documentation build assets/js/phoenix/index.js -f html -o doc/js",
|
||||
"lint": "eslint assets",
|
||||
"lint:fix": "eslint --fix assets"
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 152 B |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
+1869
File diff suppressed because it is too large
Load Diff
+7
File diff suppressed because one or more lines are too long
+1891
File diff suppressed because it is too large
Load Diff
+2
File diff suppressed because one or more lines are too long
+1846
File diff suppressed because it is too large
Load Diff
+7
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
+10
@@ -0,0 +1,10 @@
|
||||
export default class Ajax {
|
||||
static request(method: any, endPoint: any, headers: any, body: any, timeout: any, ontimeout: any, callback: any): any;
|
||||
static fetchRequest(method: any, endPoint: any, headers: any, body: any, timeout: any, ontimeout: any, callback: any): AbortController | null;
|
||||
static xdomainRequest(req: any, method: any, endPoint: any, body: any, timeout: any, ontimeout: any, callback: any): any;
|
||||
static xhrRequest(req: any, method: any, endPoint: any, headers: any, body: any, timeout: any, ontimeout: any, callback: any): any;
|
||||
static parseJSON(resp: any): any;
|
||||
static serialize(obj: any, parentKey: any): any;
|
||||
static appendParams(url: any, params: any): any;
|
||||
}
|
||||
//# sourceMappingURL=ajax.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"ajax.d.ts","sourceRoot":"","sources":["../../../assets/js/phoenix/ajax.js"],"names":[],"mappings":"AAKA;IAEE,sHAaC;IAED,8IAwBC;IAED,yHAcC;IAED,mIAiBC;IAED,iCASC;IAED,gDAaC;IAED,gDAKC;CACF"}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* @import Socket from "./socket"
|
||||
* @import { ChannelState, Params, ChannelBindingCallback, ChannelOnMessage, ChannelFilterBindings, ChannelOnErrorCallback, ChannelBinding } from "./types"
|
||||
*/
|
||||
export default class Channel {
|
||||
/**
|
||||
* @param {string} topic
|
||||
* @param {Params | (() => Params)} params
|
||||
* @param {Socket} socket
|
||||
*/
|
||||
constructor(topic: string, params: Params | (() => Params), socket: Socket);
|
||||
/** @type{ChannelState} */
|
||||
state: ChannelState;
|
||||
/** @type{string} */
|
||||
topic: string;
|
||||
/** @type{() => Params} */
|
||||
params: () => Params;
|
||||
/** @type {Socket} */
|
||||
socket: Socket;
|
||||
/** @type{ChannelBinding[]} */
|
||||
bindings: ChannelBinding[];
|
||||
/** @type{number} */
|
||||
bindingRef: number;
|
||||
/** @type{number} */
|
||||
timeout: number;
|
||||
/** @type{boolean} */
|
||||
joinedOnce: boolean;
|
||||
/** @type{Push} */
|
||||
joinPush: Push;
|
||||
/** @type{Push[]} */
|
||||
pushBuffer: Push[];
|
||||
/** @type{string[]} */
|
||||
stateChangeRefs: string[];
|
||||
/** @type{Timer} */
|
||||
rejoinTimer: Timer;
|
||||
/**
|
||||
* Join the channel
|
||||
* @param {number} timeout
|
||||
* @returns {Push}
|
||||
*/
|
||||
join(timeout?: number): Push;
|
||||
/**
|
||||
* Teardown the channel.
|
||||
*
|
||||
* Destroys and stops related timers.
|
||||
*/
|
||||
teardown(): void;
|
||||
/**
|
||||
* Hook into channel close
|
||||
* @param {ChannelBindingCallback} callback
|
||||
*/
|
||||
onClose(callback: ChannelBindingCallback): void;
|
||||
/**
|
||||
* Hook into channel errors
|
||||
* @param {ChannelOnErrorCallback} callback
|
||||
* @return {number}
|
||||
*/
|
||||
onError(callback: ChannelOnErrorCallback): number;
|
||||
/**
|
||||
* Subscribes on channel events
|
||||
*
|
||||
* Subscription returns a ref counter, which can be used later to
|
||||
* unsubscribe the exact event listener
|
||||
*
|
||||
* @example
|
||||
* const ref1 = channel.on("event", do_stuff)
|
||||
* const ref2 = channel.on("event", do_other_stuff)
|
||||
* channel.off("event", ref1)
|
||||
* // Since unsubscription, do_stuff won't fire,
|
||||
* // while do_other_stuff will keep firing on the "event"
|
||||
*
|
||||
* @param {string} event
|
||||
* @param {ChannelBindingCallback} callback
|
||||
* @returns {number} ref
|
||||
*/
|
||||
on(event: string, callback: ChannelBindingCallback): number;
|
||||
/**
|
||||
* Unsubscribes off of channel events
|
||||
*
|
||||
* Use the ref returned from a channel.on() to unsubscribe one
|
||||
* handler, or pass nothing for the ref to unsubscribe all
|
||||
* handlers for the given event.
|
||||
*
|
||||
* @example
|
||||
* // Unsubscribe the do_stuff handler
|
||||
* const ref1 = channel.on("event", do_stuff)
|
||||
* channel.off("event", ref1)
|
||||
*
|
||||
* // Unsubscribe all handlers from event
|
||||
* channel.off("event")
|
||||
*
|
||||
* @param {string} event
|
||||
* @param {number} [ref]
|
||||
*/
|
||||
off(event: string, ref?: number): void;
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
private canPush;
|
||||
/**
|
||||
* Sends a message `event` to phoenix with the payload `payload`.
|
||||
* Phoenix receives this in the `handle_in(event, payload, socket)`
|
||||
* function. if phoenix replies or it times out (default 10000ms),
|
||||
* then optionally the reply can be received.
|
||||
*
|
||||
* @example
|
||||
* channel.push("event")
|
||||
* .receive("ok", payload => console.log("phoenix replied:", payload))
|
||||
* .receive("error", err => console.log("phoenix errored", err))
|
||||
* .receive("timeout", () => console.log("timed out pushing"))
|
||||
* @param {string} event
|
||||
* @param {Object} payload
|
||||
* @param {number} [timeout]
|
||||
* @returns {Push}
|
||||
*/
|
||||
push(event: string, payload: Object, timeout?: number): Push;
|
||||
/** Leaves the channel
|
||||
*
|
||||
* Unsubscribes from server events, and
|
||||
* instructs channel to terminate on server
|
||||
*
|
||||
* Triggers onClose() hooks
|
||||
*
|
||||
* To receive leave acknowledgements, use the `receive`
|
||||
* hook to bind to the server ack, ie:
|
||||
*
|
||||
* @example
|
||||
* channel.leave().receive("ok", () => alert("left!") )
|
||||
*
|
||||
* @param {number} timeout
|
||||
* @returns {Push}
|
||||
*/
|
||||
leave(timeout?: number): Push;
|
||||
onMessage(event: string, payload?: unknown, ref?: string | null, joinRef?: string | null): unknown;
|
||||
filterBindings(binding: ChannelBinding, payload: unknown, ref?: string | null): boolean;
|
||||
isMember(topic: any, event: any, payload: any, joinRef: any): boolean;
|
||||
joinRef(): string | null | undefined;
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
private rejoin;
|
||||
/**
|
||||
* @param {string} event
|
||||
* @param {unknown} [payload]
|
||||
* @param {?string} [ref]
|
||||
* @param {?string} [joinRef]
|
||||
*/
|
||||
trigger(event: string, payload?: unknown, ref?: string | null, joinRef?: string | null): void;
|
||||
/**
|
||||
* @param {string} ref
|
||||
*/
|
||||
replyEventName(ref: string): string;
|
||||
isClosed(): boolean;
|
||||
isErrored(): boolean;
|
||||
isJoined(): boolean;
|
||||
isJoining(): boolean;
|
||||
isLeaving(): boolean;
|
||||
}
|
||||
import type { ChannelState } from "./types";
|
||||
import type { Params } from "./types";
|
||||
import type Socket from "./socket";
|
||||
import type { ChannelBinding } from "./types";
|
||||
import Push from "./push";
|
||||
import Timer from "./timer";
|
||||
import type { ChannelBindingCallback } from "./types";
|
||||
import type { ChannelOnErrorCallback } from "./types";
|
||||
//# sourceMappingURL=channel.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../../assets/js/phoenix/channel.js"],"names":[],"mappings":"AASA;;;EAGE;AAEF;IACA;;;;OAIG;IACD,mBAJS,MAAM,UACN,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,UACvB,MAAM,EAsEd;IAnEC,0BAA0B;IAC1B,OADU,YAAY,CACY;IAClC,oBAAoB;IACpB,OADU,MAAM,CACE;IAClB,0BAA0B;IAC1B,QADU,MAAM,MAAM,CACa;IACnC,qBAAqB;IACrB,QADW,MAAM,CACG;IACpB,8BAA8B;IAC9B,UADU,cAAc,EAAE,CACR;IAClB,oBAAoB;IACpB,YADU,MAAM,CACG;IACnB,oBAAoB;IACpB,SADU,MAAM,CACkB;IAClC,qBAAqB;IACrB,YADU,OAAO,CACM;IACvB,kBAAkB;IAClB,UADU,IAAI,CACgE;IAC9E,oBAAoB;IACpB,YADU,IAAI,EAAE,CACI;IACpB,sBAAsB;IACtB,iBADU,MAAM,EAAE,CACO;IAEzB,mBAAmB;IACnB,aADU,KAAK,CAGc;IA2C/B;;;;OAIG;IACH,eAHW,MAAM,GACJ,IAAI,CAWhB;IAED;;;;OAIG;IACH,iBAOC;IAED;;;OAGG;IACH,kBAFW,sBAAsB,QAIhC;IAED;;;;OAIG;IACH,kBAHW,sBAAsB,GACrB,MAAM,CAIjB;IAED;;;;;;;;;;;;;;;;OAgBG;IACH,UAJW,MAAM,YACN,sBAAsB,GACpB,MAAM,CAMlB;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,WAHW,MAAM,QACN,MAAM,QAMhB;IAED;;OAEG;IACH,gBAAgE;IAEhE;;;;;;;;;;;;;;;OAeG;IACH,YALW,MAAM,WACN,MAAM,YACN,MAAM,GACJ,IAAI,CAgBhB;IAED;;;;;;;;;;;;;;;OAeG;IACH,gBAHW,MAAM,GACJ,IAAI,CAkBhB;;;IAsBD,sEASC;IAED,qCAAqC;IAErC;;OAEG;IACH,eAKC;IAED;;;;;OAKG;IACH,eALW,MAAM,YACN,OAAO,QACN,MAAM,OAAA,YACN,MAAM,OAAA,QAYjB;IAED;;MAEE;IACF,oBAFU,MAAM,UAEiC;IAEjD,oBAAyD;IAEzD,qBAA2D;IAE3D,oBAAyD;IAEzD,qBAA2D;IAE3D,qBAA2D;CAC5D;kCA/TgJ,SAAS;4BAAT,SAAS;wBADpI,UAAU;oCACiH,SAAS;iBALzI,QAAQ;kBACP,SAAS;4CAIsH,SAAS;4CAAT,SAAS"}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
export const globalSelf: (Window & typeof globalThis) | null;
|
||||
export const phxWindow: (Window & typeof globalThis) | null;
|
||||
export const global: typeof globalThis;
|
||||
export const DEFAULT_VSN: "2.0.0";
|
||||
export const DEFAULT_TIMEOUT: 10000;
|
||||
export const WS_CLOSE_NORMAL: 1000;
|
||||
export const MAX_LONGPOLL_BATCH_SIZE: 100;
|
||||
export namespace SOCKET_STATES {
|
||||
let connecting: 0;
|
||||
let open: 1;
|
||||
let closing: 2;
|
||||
let closed: 3;
|
||||
}
|
||||
export namespace CHANNEL_STATES {
|
||||
let closed_1: "closed";
|
||||
export { closed_1 as closed };
|
||||
export let errored: "errored";
|
||||
export let joined: "joined";
|
||||
export let joining: "joining";
|
||||
export let leaving: "leaving";
|
||||
}
|
||||
export namespace CHANNEL_EVENTS {
|
||||
let close: "phx_close";
|
||||
let error: "phx_error";
|
||||
let join: "phx_join";
|
||||
let reply: "phx_reply";
|
||||
let leave: "phx_leave";
|
||||
}
|
||||
export namespace TRANSPORTS {
|
||||
let longpoll: "longpoll";
|
||||
let websocket: "websocket";
|
||||
}
|
||||
export namespace XHR_STATES {
|
||||
let complete: 4;
|
||||
}
|
||||
export const AUTH_TOKEN_PREFIX: "base64url.bearer.phx.";
|
||||
//# sourceMappingURL=constants.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../assets/js/phoenix/constants.js"],"names":[],"mappings":"AAAA,6DAAmE;AACnE,4DAAsE;AACtE,uCAA2D;AAC3D,0BAA2B,OAAO,CAAA;AAClC,8BAA+B,KAAK,CAAA;AACpC,8BAA+B,IAAI,CAAA;AACnC,sCAAuC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6B1C,gCAAiC,uBAAuB,CAAA"}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
export * from "./types";
|
||||
import Channel from "./channel";
|
||||
import LongPoll from "./longpoll";
|
||||
import Presence from "./presence";
|
||||
import Push from "./push";
|
||||
import Serializer from "./serializer";
|
||||
import Socket from "./socket";
|
||||
import Timer from "./timer";
|
||||
export { Channel, LongPoll, Presence, Push, Serializer, Socket, Timer };
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../assets/js/phoenix/index.js"],"names":[],"mappings":";oBAkMoB,WAAW;qBACV,YAAY;qBACZ,YAAY;iBAIhB,QAAQ;uBAHF,cAAc;mBAClB,UAAU;kBACX,SAAS"}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
export default class LongPoll {
|
||||
constructor(endPoint: any, protocols: any);
|
||||
authToken: string | undefined;
|
||||
endPoint: any;
|
||||
token: any;
|
||||
skipHeartbeat: boolean;
|
||||
reqs: Set<any>;
|
||||
awaitingBatchAck: boolean;
|
||||
currentBatch: any[] | null;
|
||||
currentBatchTimer: NodeJS.Timeout | null;
|
||||
batchBuffer: any[];
|
||||
onopen: () => void;
|
||||
onerror: () => void;
|
||||
onmessage: () => void;
|
||||
onclose: () => void;
|
||||
pollEndpoint: any;
|
||||
readyState: 0;
|
||||
normalizeEndpoint(endPoint: any): any;
|
||||
endpointURL(): any;
|
||||
closeAndRetry(code: any, reason: any, wasClean: any): void;
|
||||
ontimeout(): void;
|
||||
isActive(): boolean;
|
||||
poll(): void;
|
||||
send(body: any): void;
|
||||
batchSend(messages: any, offset?: number): void;
|
||||
close(code: any, reason: any, wasClean: any): void;
|
||||
ajax(method: any, headers: any, body: any, onCallerTimeout: any, callback: any): void;
|
||||
}
|
||||
//# sourceMappingURL=longpoll.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"longpoll.d.ts","sourceRoot":"","sources":["../../../assets/js/phoenix/longpoll.js"],"names":[],"mappings":"AAiBA;IAEE,2CAsBC;IAlBG,8BAAmE;IAErE,cAAoB;IACpB,WAAiB;IACjB,uBAAyB;IACzB,eAAqB;IACrB,0BAA6B;IAC7B,2BAAwB;IACxB,yCAA6B;IAC7B,mBAAqB;IACrB,mBAA4B;IAC5B,oBAA6B;IAC7B,sBAA+B;IAC/B,oBAA6B;IAC7B,kBAAoD;IACpD,cAA0C;IAK5C,sCAKC;IAED,mBAEC;IAED,2DAGC;IAED,kBAGC;IAED,oBAA2G;IAE3G,aAiEC;IAMD,sBAaC;IAED,gDAkBC;IAED,mDAYC;IAED,sFAWC;CACF"}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* @import Channel from "./channel"
|
||||
* @import { PresenceEvents, PresenceOnJoin, PresenceOnLeave, PresenceOnSync, PresenceState, PresenceDiff, PresenceOptions } from "./types"
|
||||
*/
|
||||
export default class Presence {
|
||||
/**
|
||||
* Used to sync the list of presences on the server
|
||||
* with the client's state. An optional `onJoin` and `onLeave` callback can
|
||||
* be provided to react to changes in the client's local presences across
|
||||
* disconnects and reconnects with the server.
|
||||
*
|
||||
* @param {Record<string, PresenceState>} currentState
|
||||
* @param {Record<string, PresenceState>} newState
|
||||
* @param {PresenceOnJoin} onJoin
|
||||
* @param {PresenceOnLeave} onLeave
|
||||
*
|
||||
* @returns {Record<string, PresenceState>}
|
||||
*/
|
||||
static syncState(currentState: Record<string, PresenceState>, newState: Record<string, PresenceState>, onJoin: PresenceOnJoin, onLeave: PresenceOnLeave): Record<string, PresenceState>;
|
||||
/**
|
||||
*
|
||||
* Used to sync a diff of presence join and leave
|
||||
* events from the server, as they happen. Like `syncState`, `syncDiff`
|
||||
* accepts optional `onJoin` and `onLeave` callbacks to react to a user
|
||||
* joining or leaving from a device.
|
||||
*
|
||||
* @param {Record<string, PresenceState>} state
|
||||
* @param {PresenceDiff} diff
|
||||
* @param {PresenceOnJoin} onJoin
|
||||
* @param {PresenceOnLeave} onLeave
|
||||
*
|
||||
* @returns {Record<string, PresenceState>}
|
||||
*/
|
||||
static syncDiff(state: Record<string, PresenceState>, diff: PresenceDiff, onJoin: PresenceOnJoin, onLeave: PresenceOnLeave): Record<string, PresenceState>;
|
||||
/**
|
||||
* Returns the array of presences, with selected metadata.
|
||||
*
|
||||
* @template [T=PresenceState]
|
||||
* @param {Record<string, PresenceState>} presences
|
||||
* @param {((key: string, obj: PresenceState) => T)} [chooser]
|
||||
*
|
||||
* @returns {T[]}
|
||||
*/
|
||||
static list<T = PresenceState>(presences: Record<string, PresenceState>, chooser?: ((key: string, obj: PresenceState) => T)): T[];
|
||||
/**
|
||||
* @template T
|
||||
* @param {Record<string, PresenceState>} obj
|
||||
* @param {(key: string, obj: PresenceState) => T} func
|
||||
*/
|
||||
static map<T>(obj: Record<string, PresenceState>, func: (key: string, obj: PresenceState) => T): T[];
|
||||
static toNullProtoObj(obj: any): any;
|
||||
/**
|
||||
* @template T
|
||||
* @param {T} obj
|
||||
* @returns {T}
|
||||
*/
|
||||
static clone<T>(obj: T): T;
|
||||
/**
|
||||
* Initializes the Presence
|
||||
* @param {Channel} channel - The Channel
|
||||
* @param {PresenceOptions} [opts] - The options, for example `{events: {state: "state", diff: "diff"}}`
|
||||
*/
|
||||
constructor(channel: Channel, opts?: PresenceOptions);
|
||||
/** @type{Record<string, PresenceState>} */
|
||||
state: Record<string, PresenceState>;
|
||||
/** @type{PresenceDiff[]} */
|
||||
pendingDiffs: PresenceDiff[];
|
||||
/** @type{Channel} */
|
||||
channel: Channel;
|
||||
/** @type{?number} */
|
||||
joinRef: number | null;
|
||||
/** @type{({ onJoin: PresenceOnJoin; onLeave: PresenceOnLeave; onSync: PresenceOnSync })} */
|
||||
caller: ({
|
||||
onJoin: PresenceOnJoin;
|
||||
onLeave: PresenceOnLeave;
|
||||
onSync: PresenceOnSync;
|
||||
});
|
||||
/**
|
||||
* @param {PresenceOnJoin} callback
|
||||
*/
|
||||
onJoin(callback: PresenceOnJoin): void;
|
||||
/**
|
||||
* @param {PresenceOnLeave} callback
|
||||
*/
|
||||
onLeave(callback: PresenceOnLeave): void;
|
||||
/**
|
||||
* @param {PresenceOnSync} callback
|
||||
*/
|
||||
onSync(callback: PresenceOnSync): void;
|
||||
/**
|
||||
* Returns the array of presences, with selected metadata.
|
||||
*
|
||||
* @template [T=PresenceState]
|
||||
* @param {((key: string, obj: PresenceState) => T)} [by]
|
||||
*
|
||||
* @returns {T[]}
|
||||
*/
|
||||
list<T = PresenceState>(by?: ((key: string, obj: PresenceState) => T)): T[];
|
||||
inPendingSyncState(): boolean;
|
||||
}
|
||||
import type { PresenceState } from "./types";
|
||||
import type { PresenceDiff } from "./types";
|
||||
import type Channel from "./channel";
|
||||
import type { PresenceOnJoin } from "./types";
|
||||
import type { PresenceOnLeave } from "./types";
|
||||
import type { PresenceOnSync } from "./types";
|
||||
import type { PresenceOptions } from "./types";
|
||||
//# sourceMappingURL=presence.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"presence.d.ts","sourceRoot":"","sources":["../../../assets/js/phoenix/presence.js"],"names":[],"mappings":"AAAA;;;GAGG;AACH;IAgFE;;;;;;;;;;;;OAYG;IACH,+BAPW,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,YAC7B,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,UAC7B,cAAc,WACd,eAAe,GAEb,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAiCzC;IAED;;;;;;;;;;;;;OAaG;IACH,uBAPW,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,QAC7B,YAAY,UACZ,cAAc,WACd,eAAe,GAEb,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CA+BzC;IAED;;;;;;;;OAQG;IACH,YANc,CAAC,6BACJ,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,YAC7B,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,KAAK,CAAC,CAAC,GAEtC,CAAC,EAAE,CAQf;IAID;;;;MAIE;IACF,WAJY,CAAC,OACH,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,QAC7B,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,KAAK,CAAC,OAI/C;IAQD,qCAOC;IAED;;;;MAIE;IACF,aAJY,CAAC,OACH,CAAC,GACC,CAAC,CAE8C;IAzN3D;;;;OAIG;IACH,qBAHW,OAAO,SACP,eAAe,EA0CzB;IAtCC,2CAA2C;IAC3C,OADU,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CACP;IAChC,4BAA4B;IAC5B,cADU,YAAY,EAAE,CACF;IACtB,qBAAqB;IACrB,SADU,OAAO,CACK;IACtB,qBAAqB;IACrB,SADW,MAAM,OAAA,CACE;IACnB,4FAA4F;IAC5F,QADU,CAAC;QAAE,MAAM,EAAE,cAAc,CAAC;QAAC,OAAO,EAAE,eAAe,CAAC;QAAC,MAAM,EAAE,cAAc,CAAA;KAAE,CAAC,CAKvF;IA2BH;;OAEG;IACH,iBAFW,cAAc,QAEwB;IAEjD;;OAEG;IACH,kBAFW,eAAe,QAEyB;IAEnD;;OAEG;IACH,iBAFW,cAAc,QAEwB;IAEjD;;;;;;;OAOG;IACH,KALc,CAAC,uBACJ,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,KAAK,CAAC,CAAC,GAEtC,CAAC,EAAE,CAEgC;IAEhD,8BAEC;CAgJF;mCA9NiI,SAAS;kCAAT,SAAS;yBADnH,WAAW;oCAC+F,SAAS;qCAAT,SAAS;oCAAT,SAAS;qCAAT,SAAS"}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @import Channel from "./channel"
|
||||
* @import { ChannelEvent } from "./types"
|
||||
*/
|
||||
export default class Push {
|
||||
/**
|
||||
* Initializes the Push
|
||||
* @param {Channel} channel - The Channel
|
||||
* @param {ChannelEvent} event - The event, for example `"phx_join"`
|
||||
* @param {() => Record<string, unknown>} payload - The payload, for example `{user_id: 123}`
|
||||
* @param {number} timeout - The push timeout in milliseconds
|
||||
*/
|
||||
constructor(channel: Channel, event: ChannelEvent, payload: () => Record<string, unknown>, timeout: number);
|
||||
/** @type{Channel} */
|
||||
channel: Channel;
|
||||
/** @type{ChannelEvent} */
|
||||
event: ChannelEvent;
|
||||
/** @type{() => Record<string, unknown>} */
|
||||
payload: () => Record<string, unknown>;
|
||||
receivedResp: unknown;
|
||||
/** @type{number} */
|
||||
timeout: number;
|
||||
/** @type{(ReturnType<typeof setTimeout>) | null} */
|
||||
timeoutTimer: (ReturnType<typeof setTimeout>) | null;
|
||||
/** @type{{status: string; callback: (response: any) => void}[]} */
|
||||
recHooks: {
|
||||
status: string;
|
||||
callback: (response: any) => void;
|
||||
}[];
|
||||
/** @type{boolean} */
|
||||
sent: boolean;
|
||||
/** @type{string | null | undefined} */
|
||||
ref: string | null | undefined;
|
||||
/**
|
||||
*
|
||||
* @param {number} timeout
|
||||
*/
|
||||
resend(timeout: number): void;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
send(): void;
|
||||
/**
|
||||
*
|
||||
* @param {string} status
|
||||
* @param {(response: any) => void} callback
|
||||
*/
|
||||
receive(status: string, callback: (response: any) => void): this;
|
||||
reset(): void;
|
||||
refEvent: string | null | undefined;
|
||||
destroy(): void;
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
private matchReceive;
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
private cancelRefEvent;
|
||||
cancelTimeout(): void;
|
||||
startTimeout(): void;
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
private hasReceived;
|
||||
trigger(status: any, response: any): void;
|
||||
}
|
||||
import type Channel from "./channel";
|
||||
import type { ChannelEvent } from "./types";
|
||||
//# sourceMappingURL=push.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"push.d.ts","sourceRoot":"","sources":["../../../assets/js/phoenix/push.js"],"names":[],"mappings":"AAAA;;;GAGG;AACH;IACE;;;;;;OAMG;IACH,qBALW,OAAO,SACP,YAAY,WACZ,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WAC7B,MAAM,EAoBhB;IAjBC,qBAAqB;IACrB,SADU,OAAO,CACK;IACtB,0BAA0B;IAC1B,OADU,YAAY,CACJ;IAClB,2CAA2C;IAC3C,SADU,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CACW;IAClD,sBAAwB;IACxB,oBAAoB;IACpB,SADU,MAAM,CACM;IACtB,oDAAoD;IACpD,cADU,CAAC,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC,GAAG,IAAI,CACxB;IACxB,mEAAmE;IACnE,UADU;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,CAAA;KAAC,EAAE,CAC7C;IAClB,qBAAqB;IACrB,MADU,OAAO,CACA;IACjB,uCAAuC;IACvC,KADU,MAAM,GAAG,IAAI,GAAG,SAAS,CACf;IAGtB;;;OAGG;IACH,gBAFW,MAAM,QAMhB;IAED;;OAEG;IACH,aAWC;IAED;;;;OAIG;IACH,gBAHW,MAAM,YACN,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,QASjC;IAED,cAMC;IAHC,oCAAoB;IAKtB,gBAGC;IAED;;OAEG;IACH,qBAGC;IAED;;OAEG;IACH,uBAGC;IAED,sBAGC;IAED,qBAeC;IAED;;OAEG;IACH,oBAEC;IAED,0CAEC;CACF;yBApIuB,WAAW;kCACF,SAAS"}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
declare namespace _default {
|
||||
let HEADER_LENGTH: number;
|
||||
let META_LENGTH: number;
|
||||
namespace KINDS {
|
||||
let push: number;
|
||||
let reply: number;
|
||||
let broadcast: number;
|
||||
}
|
||||
/**
|
||||
* @template T
|
||||
* @param {Message<Record<string, any>>} msg
|
||||
* @param {(msg: ArrayBuffer | string) => T} callback
|
||||
* @returns {T}
|
||||
*/
|
||||
function encode<T>(msg: Message<Record<string, any>>, callback: (msg: ArrayBuffer | string) => T): T;
|
||||
/**
|
||||
* @template T
|
||||
* @param {ArrayBuffer | string} rawPayload
|
||||
* @param {(msg: Message<unknown>) => T} callback
|
||||
* @returns {T}
|
||||
*/
|
||||
function decode<T>(rawPayload: ArrayBuffer | string, callback: (msg: Message<unknown>) => T): T;
|
||||
/** @private */
|
||||
function binaryEncode(message: any): any;
|
||||
function assertFieldSize(size: any, name: any): void;
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function binaryDecode(buffer: any): {
|
||||
join_ref: any;
|
||||
ref: null;
|
||||
topic: any;
|
||||
event: any;
|
||||
payload: any;
|
||||
} | {
|
||||
join_ref: any;
|
||||
ref: any;
|
||||
topic: any;
|
||||
event: "phx_reply";
|
||||
payload: {
|
||||
status: any;
|
||||
response: any;
|
||||
};
|
||||
} | undefined;
|
||||
/** @private */
|
||||
function decodePush(buffer: any, view: any, decoder: any): {
|
||||
join_ref: any;
|
||||
ref: null;
|
||||
topic: any;
|
||||
event: any;
|
||||
payload: any;
|
||||
};
|
||||
/** @private */
|
||||
function decodeReply(buffer: any, view: any, decoder: any): {
|
||||
join_ref: any;
|
||||
ref: any;
|
||||
topic: any;
|
||||
event: "phx_reply";
|
||||
payload: {
|
||||
status: any;
|
||||
response: any;
|
||||
};
|
||||
};
|
||||
/** @private */
|
||||
function decodeBroadcast(buffer: any, view: any, decoder: any): {
|
||||
join_ref: null;
|
||||
ref: null;
|
||||
topic: any;
|
||||
event: any;
|
||||
payload: any;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
import type { Message } from "./types";
|
||||
//# sourceMappingURL=serializer.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../../../assets/js/phoenix/serializer.js"],"names":[],"mappings":";;;;;;;;IAcE;;;;;MAKE;IACF,gBALY,CAAC,OACH,QAAQ,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,YAC5B,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,KAAK,CAAC,GAC9B,CAAC,CASZ;IAED;;;;;MAKE;IACF,gBALY,CAAC,cACH,WAAW,GAAG,MAAM,YACpB,CAAC,GAAG,EAAE,QAAQ,OAAO,CAAC,KAAK,CAAC,GAC1B,CAAC,CASZ;IAED,eAAe;IACf,yCAkCC;IAED,qDAIC;IAED;;MAEE;IACF;;;;;;;;;;;;;;;kBASC;IAED,eAAe;IACf;;;;;;MAaC;IAED,eAAe;IACf;;;;;;;;;MAiBC;IAED,eAAe;IACf;;;;;;MAWC;;;6BA/IyB,SAAS"}
|
||||
+291
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* @import { Encode, Decode, Message, Vsn, SocketTransport, Params, SocketOnOpen, SocketOnClose, SocketOnError, SocketOnMessage, SocketOptions, SocketStateChangeCallbacks, HeartbeatCallback } from "./types"
|
||||
*/
|
||||
export default class Socket {
|
||||
/** Initializes the Socket *
|
||||
*
|
||||
* For IE8 support use an ES5-shim (https://github.com/es-shims/es5-shim)
|
||||
*
|
||||
* @constructor
|
||||
* @param {string} endPoint - The string WebSocket endpoint, ie, `"ws://example.com/socket"`,
|
||||
* `"wss://example.com"`
|
||||
* `"/socket"` (inherited host & protocol)
|
||||
* @param {SocketOptions} [opts] - Optional configuration
|
||||
*/
|
||||
constructor(endPoint: string, opts?: SocketOptions);
|
||||
/** @type{SocketStateChangeCallbacks} */
|
||||
stateChangeCallbacks: SocketStateChangeCallbacks;
|
||||
/** @type{Channel[]} */
|
||||
channels: Channel[];
|
||||
/** @type{(() => void)[]} */
|
||||
sendBuffer: (() => void)[];
|
||||
/** @type{number} */
|
||||
ref: number;
|
||||
/** @type{?string} */
|
||||
fallbackRef: string | null;
|
||||
/** @type{number} */
|
||||
timeout: number;
|
||||
/** @type{SocketTransport} */
|
||||
transport: SocketTransport;
|
||||
/** @type{InstanceType<SocketTransport> | undefined | null} */
|
||||
conn: InstanceType<SocketTransport> | undefined | null;
|
||||
/** @type{boolean} */
|
||||
primaryPassedHealthCheck: boolean;
|
||||
/** @type{number | undefined} */
|
||||
longPollFallbackMs: number | undefined;
|
||||
/** @type{ReturnType<typeof setTimeout>} */
|
||||
fallbackTimer: ReturnType<typeof setTimeout>;
|
||||
/** @type{Storage} */
|
||||
sessionStore: Storage;
|
||||
/** @type{number} */
|
||||
establishedConnections: number;
|
||||
/** @type{Encode<void>} */
|
||||
defaultEncoder: Encode<void>;
|
||||
/** @type{Decode<void>} */
|
||||
defaultDecoder: Decode<void>;
|
||||
/** @type{boolean} */
|
||||
closeWasClean: boolean;
|
||||
/** @type{boolean} */
|
||||
disconnecting: boolean;
|
||||
/** @type{BinaryType} */
|
||||
binaryType: BinaryType;
|
||||
/** @type{number} */
|
||||
connectClock: number;
|
||||
/** @type{boolean} */
|
||||
pageHidden: boolean;
|
||||
/** @type{Encode<void>} */
|
||||
encode: Encode<void>;
|
||||
/** @type{Decode<void>} */
|
||||
decode: Decode<void>;
|
||||
/** @type{number} */
|
||||
heartbeatIntervalMs: number;
|
||||
/** @type{boolean} */
|
||||
autoSendHeartbeat: boolean;
|
||||
/** @type{HeartbeatCallback} */
|
||||
heartbeatCallback: HeartbeatCallback;
|
||||
/** @type{(tries: number) => number} */
|
||||
rejoinAfterMs: (tries: number) => number;
|
||||
/** @type{(tries: number) => number} */
|
||||
reconnectAfterMs: (tries: number) => number;
|
||||
/** @type{((kind: string, msg: string, data: any) => void) | null} */
|
||||
logger: ((kind: string, msg: string, data: any) => void) | null;
|
||||
/** @type{number} */
|
||||
longpollerTimeout: number;
|
||||
/** @type{() => Params} */
|
||||
params: () => Params;
|
||||
/** @type{string} */
|
||||
endPoint: string;
|
||||
/** @type{Vsn} */
|
||||
vsn: Vsn;
|
||||
/** @type{ReturnType<typeof setTimeout>} */
|
||||
heartbeatTimeoutTimer: ReturnType<typeof setTimeout>;
|
||||
/** @type{ReturnType<typeof setTimeout>} */
|
||||
heartbeatTimer: ReturnType<typeof setTimeout>;
|
||||
/** @type{number | null} */
|
||||
heartbeatSentAt: number | null;
|
||||
/** @type{?string} */
|
||||
pendingHeartbeatRef: string | null;
|
||||
/** @type{Timer} */
|
||||
reconnectTimer: Timer;
|
||||
/** @type{(() => string) | undefined} */
|
||||
authToken: (() => string) | undefined;
|
||||
/**
|
||||
* Returns the LongPoll transport reference
|
||||
*/
|
||||
getLongPollTransport(): typeof LongPoll;
|
||||
/**
|
||||
* Disconnects and replaces the active transport
|
||||
*
|
||||
* @param {SocketTransport} newTransport - The new transport class to instantiate
|
||||
*
|
||||
*/
|
||||
replaceTransport(newTransport: SocketTransport): void;
|
||||
/**
|
||||
* Returns the socket protocol
|
||||
*
|
||||
* @returns {"wss" | "ws"}
|
||||
*/
|
||||
protocol(): "wss" | "ws";
|
||||
/**
|
||||
* The fully qualified socket url
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
endPointURL(): string;
|
||||
/**
|
||||
* Disconnects the socket
|
||||
*
|
||||
* See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes for valid status codes.
|
||||
*
|
||||
* @param {() => void} [callback] - Optional callback which is called after socket is disconnected.
|
||||
* @param {number} [code] - A status code for disconnection (Optional).
|
||||
* @param {string} [reason] - A textual description of the reason to disconnect. (Optional)
|
||||
*/
|
||||
disconnect(callback?: () => void, code?: number, reason?: string): void;
|
||||
/**
|
||||
* @param {Params} [params] - [DEPRECATED] The params to send when connecting, for example `{user_id: userToken}`
|
||||
*
|
||||
* Passing params to connect is deprecated; pass them in the Socket constructor instead:
|
||||
* `new Socket("/socket", {params: {user_id: userToken}})`.
|
||||
*/
|
||||
connect(params?: Params): void;
|
||||
/**
|
||||
* Logs the message. Override `this.logger` for specialized logging. noops by default
|
||||
* @param {string} kind
|
||||
* @param {string} msg
|
||||
* @param {Object} data
|
||||
*/
|
||||
log(kind: string, msg: string, data: Object): void;
|
||||
/**
|
||||
* Returns true if a logger has been set on this socket.
|
||||
*/
|
||||
hasLogger(): boolean;
|
||||
/**
|
||||
* Registers callbacks for connection open events
|
||||
*
|
||||
* @example socket.onOpen(function(){ console.info("the socket was opened") })
|
||||
*
|
||||
* @param {SocketOnOpen} callback
|
||||
*/
|
||||
onOpen(callback: SocketOnOpen): string;
|
||||
/**
|
||||
* Registers callbacks for connection close events
|
||||
* @param {SocketOnClose} callback
|
||||
* @returns {string}
|
||||
*/
|
||||
onClose(callback: SocketOnClose): string;
|
||||
/**
|
||||
* Registers callbacks for connection error events
|
||||
*
|
||||
* @example socket.onError(function(error){ alert("An error occurred") })
|
||||
*
|
||||
* @param {SocketOnError} callback
|
||||
* @returns {string}
|
||||
*/
|
||||
onError(callback: SocketOnError): string;
|
||||
/**
|
||||
* Registers callbacks for connection message events
|
||||
* @param {SocketOnMessage} callback
|
||||
* @returns {string}
|
||||
*/
|
||||
onMessage(callback: SocketOnMessage): string;
|
||||
/**
|
||||
* Sets a callback that receives lifecycle events for internal heartbeat messages.
|
||||
* Useful for instrumenting connection health (e.g. sent/ok/timeout/disconnected).
|
||||
* @param {HeartbeatCallback} callback
|
||||
*/
|
||||
onHeartbeat(callback: HeartbeatCallback): void;
|
||||
/**
|
||||
* Pings the server and invokes the callback with the RTT in milliseconds
|
||||
* @param {(timeDelta: number) => void} callback
|
||||
*
|
||||
* Returns true if the ping was pushed or false if unable to be pushed.
|
||||
*/
|
||||
ping(callback: (timeDelta: number) => void): boolean;
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {Function}
|
||||
*/
|
||||
private transportName;
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
private transportConnect;
|
||||
getSession(key: any): string | null;
|
||||
storeSession(key: any, val: any): void;
|
||||
connectWithFallback(fallbackTransport: any, fallbackThreshold?: number): void;
|
||||
clearHeartbeats(): void;
|
||||
onConnOpen(): void;
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
private heartbeatTimeout;
|
||||
resetHeartbeat(): void;
|
||||
teardown(callback: any, code: any, reason: any): any;
|
||||
waitForBufferDone(conn: any, callback: any, tries?: number): void;
|
||||
waitForSocketClosed(conn: any, callback: any, tries?: number): void;
|
||||
/**
|
||||
* @param {CloseEvent} event
|
||||
*/
|
||||
onConnClose(event: CloseEvent): void;
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} error
|
||||
*/
|
||||
private onConnError;
|
||||
/**
|
||||
* @private
|
||||
* @param {unknown} [reason] underlying close/error event forwarded to channel error listeners
|
||||
*/
|
||||
private triggerChanError;
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
connectionState(): string;
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isConnected(): boolean;
|
||||
/**
|
||||
*
|
||||
* @param {Channel} channel
|
||||
*/
|
||||
remove(channel: Channel): void;
|
||||
/**
|
||||
* Removes `onOpen`, `onClose`, `onError,` and `onMessage` registrations.
|
||||
*
|
||||
* @param {string[]} refs - list of refs returned by calls to
|
||||
* `onOpen`, `onClose`, `onError,` and `onMessage`
|
||||
*/
|
||||
off(refs: string[]): void;
|
||||
/**
|
||||
* Initiates a new channel for the given topic
|
||||
*
|
||||
* @param {string} topic
|
||||
* @param {Params | (() => Params)} [chanParams]- Parameters for the channel
|
||||
* @returns {Channel}
|
||||
*/
|
||||
channel(topic: string, chanParams?: Params | (() => Params)): Channel;
|
||||
/**
|
||||
* @param {Message<Record<string, any>>} data
|
||||
*/
|
||||
push(data: Message<Record<string, any>>): void;
|
||||
/**
|
||||
* Return the next message ref, accounting for overflows
|
||||
* @returns {string}
|
||||
*/
|
||||
makeRef(): string;
|
||||
sendHeartbeat(): void;
|
||||
flushSendBuffer(): void;
|
||||
/**
|
||||
* @param {MessageEvent<any>} rawMessage
|
||||
*/
|
||||
onConnMessage(rawMessage: MessageEvent<any>): void;
|
||||
/**
|
||||
* @private
|
||||
* @template {keyof SocketStateChangeCallbacks} K
|
||||
* @param {K} event
|
||||
* @param {...Parameters<SocketStateChangeCallbacks[K][number][1]>} args
|
||||
* @returns {void}
|
||||
*/
|
||||
private triggerStateCallbacks;
|
||||
leaveOpenTopic(topic: any): void;
|
||||
}
|
||||
import type { SocketStateChangeCallbacks } from "./types";
|
||||
import Channel from "./channel";
|
||||
import type { SocketTransport } from "./types";
|
||||
import type { Encode } from "./types";
|
||||
import type { Decode } from "./types";
|
||||
import type { HeartbeatCallback } from "./types";
|
||||
import type { Params } from "./types";
|
||||
import type { Vsn } from "./types";
|
||||
import Timer from "./timer";
|
||||
import LongPoll from "./longpoll";
|
||||
import type { SocketOnOpen } from "./types";
|
||||
import type { SocketOnClose } from "./types";
|
||||
import type { SocketOnError } from "./types";
|
||||
import type { SocketOnMessage } from "./types";
|
||||
import type { Message } from "./types";
|
||||
import type { SocketOptions } from "./types";
|
||||
//# sourceMappingURL=socket.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"socket.d.ts","sourceRoot":"","sources":["../../../assets/js/phoenix/socket.js"],"names":[],"mappings":"AAsBA;;EAEE;AAEF;IACE;;;;;;;;;OASG;IACH,sBALW,MAAM,SAGN,aAAa,EAoJvB;IAjJC,wCAAwC;IACxC,sBADU,0BAA0B,CACqC;IACzE,uBAAuB;IACvB,UADU,OAAO,EAAE,CACD;IAClB,4BAA4B;IAC5B,YADU,CAAC,MAAM,IAAI,CAAC,EAAE,CACJ;IACpB,oBAAoB;IACpB,KADU,MAAM,CACJ;IACZ,qBAAqB;IACrB,aADW,MAAM,OAAA,CACM;IACvB,oBAAoB;IACpB,SADU,MAAM,CAC8B;IAC9C,6BAA6B;IAC7B,WADU,eAAe,CACsC;IAC/D,8DAA8D;IAC9D,MADU,YAAY,CAAC,eAAe,CAAC,GAAG,SAAS,GAAG,IAAI,CACrC;IACrB,qBAAqB;IACrB,0BADU,OAAO,CACoB;IACrC,gCAAgC;IAChC,oBADU,MAAM,GAAG,SAAS,CACqB;IACjD,2CAA2C;IAC3C,eADU,UAAU,CAAC,OAAO,UAAU,CAAC,CACd;IAOzB,qBAAqB;IACrB,cADU,OAAO,CAC2C;IAC5D,oBAAoB;IACpB,wBADU,MAAM,CACe;IAC/B,0BAA0B;IAC1B,gBADU,OAAO,IAAI,CAAC,CACkC;IACxD,0BAA0B;IAC1B,gBADU,OAAO,IAAI,CAAC,CACkC;IAIxD,qBAAqB;IACrB,eADU,OAAO,CACQ;IACzB,qBAAqB;IACrB,eADU,OAAO,CACS;IAC1B,wBAAwB;IACxB,YADU,UAAU,CAC8B;IAClD,oBAAoB;IACpB,cADU,MAAM,CACK;IACrB,qBAAqB;IACrB,YADU,OAAO,CACM;IACvB,0BAA0B;IAC1B,QADU,OAAO,IAAI,CAAC,CACC;IACvB,0BAA0B;IAC1B,QADU,OAAO,IAAI,CAAC,CACC;IAmCvB,oBAAoB;IACpB,qBADU,MAAM,CAC4C;IAC5D,qBAAqB;IACrB,mBADU,OAAO,CACsC;IACvD,+BAA+B;IAC/B,mBADU,iBAAiB,CACkC;IAC7D,uCAAuC;IACvC,eADU,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAOlC;IACD,uCAAuC;IACvC,kBADU,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAOlC;IACD,qEAAqE;IACrE,QADU,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,IAAI,CAAC,GAAG,IAAI,CAChC;IAIjC,oBAAoB;IACpB,mBADU,MAAM,CACwC;IACxD,0BAA0B;IAC1B,QADU,MAAM,MAAM,CACkB;IACxC,oBAAoB;IACpB,UADU,MAAM,CACqC;IACrD,iBAAiB;IACjB,KADU,GAAG,CACqB;IAClC,2CAA2C;IAC3C,uBADU,UAAU,CAAC,OAAO,UAAU,CAAC,CACN;IACjC,2CAA2C;IAC3C,gBADU,UAAU,CAAC,OAAO,UAAU,CAAC,CACb;IAC1B,2BAA2B;IAC3B,iBADU,MAAM,GAAG,IAAI,CACI;IAC3B,qBAAqB;IACrB,qBADW,MAAM,OAAA,CACc;IAC/B,mBAAmB;IACnB,gBADU,KAAK,CAYU;IACzB,wCAAwC;IACxC,WADU,CAAC,MAAM,MAAM,CAAC,GAAG,SAAS,CACsB;IAG5D;;OAEG;IACH,wCAAyC;IAEzC;;;;;OAKG;IACH,+BAHW,eAAe,QAazB;IAED;;;;OAIG;IACH,YAFa,KAAK,GAAG,IAAI,CAE4C;IAErE;;;;OAIG;IACH,eAFa,MAAM,CASlB;IAED;;;;;;;;OAQG;IACH,sBAJW,MAAM,IAAI,SACV,MAAM,WACN,MAAM,QAYhB;IAED;;;;;OAKG;IACH,iBALW,MAAM,QAgBhB;IAED;;;;;OAKG;IACH,UAJW,MAAM,OACN,MAAM,QACN,MAAM,QAEkD;IAEnE;;OAEG;IACH,qBAA0C;IAE1C;;;;;;OAMG;IACH,iBAFW,YAAY,UAMtB;IAED;;;;OAIG;IACH,kBAHW,aAAa,GACX,MAAM,CAMlB;IAED;;;;;;;OAOG;IACH,kBAHW,aAAa,GACX,MAAM,CAMlB;IAED;;;;OAIG;IACH,oBAHW,eAAe,GACb,MAAM,CAMlB;IAED;;;;OAIG;IACH,sBAFW,iBAAiB,QAI3B;IAED;;;;;OAKG;IACH,eAJW,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,WAgBrC;IAED;;;;OAIG;IACH,sBAWC;IAED;;OAEG;IACH,yBAgBC;IAED,oCAA6E;IAE7E,uCAAkF;IAElF,8EA6CC;IAED,wBAGC;IAED,mBAWC;IAED;;OAEG;IAEH,yBAcC;IAED,uBAKC;IAED,qDAwBC;IAED,kEASC;IAED,oEASC;IAED;;MAEE;IACF,mBAFU,UAAU,QAWnB;IAED;;;OAGG;IACH,oBAQC;IAED;;;OAGG;IACH,yBAMC;IAED;;OAEG;IACH,mBAFa,MAAM,CASlB;IAED;;OAEG;IACH,eAFa,OAAO,CAEqC;IAEzD;;;OAGG;IACH,gBAFW,OAAO,QAKjB;IAED;;;;;OAKG;IACH,UAHW,MAAM,EAAE,QASlB;IAED;;;;;;OAMG;IACH,eAJW,MAAM,eACN,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,GACrB,OAAO,CAMnB;IAED;;OAEG;IACH,WAFW,QAAQ,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,QAatC;IAED;;;OAGG;IACH,WAFa,MAAM,CAOlB;IAED,sBAsBC;IAED,wBAKC;IAED;;MAEE;IACF,0BAFU,YAAY,CAAC,GAAG,CAAC,QA8B1B;IAED;;;;;;OAMG;IACH,8BAYC;IAED,iCAMC;CACF;gDA5tBmM,SAAS;oBANzL,WAAW;qCAMqK,SAAS;4BAAT,SAAS;4BAAT,SAAS;uCAAT,SAAS;4BAAT,SAAS;yBAAT,SAAS;kBAH3L,SAAS;qBAFN,YAAY;kCAKmK,SAAS;mCAAT,SAAS;mCAAT,SAAS;qCAAT,SAAS;6BAAT,SAAS;mCAAT,SAAS"}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
*
|
||||
* Creates a timer that accepts a `timerCalc` function to perform
|
||||
* calculated timeout retries, such as exponential backoff.
|
||||
*
|
||||
* @example
|
||||
* let reconnectTimer = new Timer(() => this.connect(), function(tries){
|
||||
* return [1000, 5000, 10000][tries - 1] || 10000
|
||||
* })
|
||||
* reconnectTimer.scheduleTimeout() // fires after 1000
|
||||
* reconnectTimer.scheduleTimeout() // fires after 5000
|
||||
* reconnectTimer.reset()
|
||||
* reconnectTimer.scheduleTimeout() // fires after 1000
|
||||
*
|
||||
*/
|
||||
export default class Timer {
|
||||
/**
|
||||
* @param {() => void} callback
|
||||
* @param {(tries: number) => number} timerCalc
|
||||
*/
|
||||
constructor(callback: () => void, timerCalc: (tries: number) => number);
|
||||
/** @type {() => void} */
|
||||
callback: () => void;
|
||||
/** @type {(tries: number) => number} */
|
||||
timerCalc: (tries: number) => number;
|
||||
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
||||
timer: ReturnType<typeof setTimeout> | undefined;
|
||||
/** @type {number} */
|
||||
tries: number;
|
||||
reset(): void;
|
||||
/**
|
||||
* Cancels any previous scheduleTimeout and schedules callback
|
||||
*/
|
||||
scheduleTimeout(): void;
|
||||
}
|
||||
//# sourceMappingURL=timer.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"timer.d.ts","sourceRoot":"","sources":["../../../assets/js/phoenix/timer.js"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH;IACE;;;MAGE;IACF,sBAHU,MAAM,IAAI,aACV,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,EAWlC;IARC,yBAAyB;IACzB,UADW,MAAM,IAAI,CACG;IACxB,wCAAwC;IACxC,WADW,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CACV;IAC1B,wDAAwD;IACxD,OADW,UAAU,CAAC,OAAO,UAAU,CAAC,GAAG,SAAS,CAC9B;IACtB,qBAAqB;IACrB,OADW,MAAM,CACH;IAGhB,cAGC;IAED;;OAEG;IACH,wBAOC;CACF"}
|
||||
+280
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* MISC
|
||||
*/
|
||||
export type Params = Record<string, unknown>;
|
||||
export type Closure<T> = T | (() => T);
|
||||
/**
|
||||
* CHANNEL
|
||||
*/
|
||||
export type ChannelBindingCallback = (payload: unknown, ref: string | null | undefined, joinRef: string) => void;
|
||||
/**
|
||||
* CHANNEL
|
||||
*/
|
||||
export type ChannelOnErrorCallback = (reason: unknown) => void;
|
||||
/**
|
||||
* CHANNEL
|
||||
*/
|
||||
export type ChannelBinding = ({
|
||||
event: string;
|
||||
ref: number;
|
||||
callback: ChannelBindingCallback;
|
||||
});
|
||||
/**
|
||||
* CHANNEL
|
||||
*/
|
||||
export type ChannelOnMessage = (event: string, payload?: unknown, ref?: string | null, joinRef?: string | null) => unknown;
|
||||
/**
|
||||
* CHANNEL
|
||||
*/
|
||||
export type ChannelFilterBindings = (binding: ChannelBinding, payload: unknown, ref?: string | null) => boolean;
|
||||
/**
|
||||
* CONSTANTS
|
||||
*/
|
||||
export type Vsn = "1.0.0" | "2.0.0";
|
||||
/**
|
||||
* CONSTANTS
|
||||
*/
|
||||
export type SocketState = (typeof SOCKET_STATES)[keyof typeof SOCKET_STATES];
|
||||
/**
|
||||
* CONSTANTS
|
||||
*/
|
||||
export type ChannelState = (typeof CHANNEL_STATES)[keyof typeof CHANNEL_STATES];
|
||||
/**
|
||||
* CONSTANTS
|
||||
*/
|
||||
export type ChannelEvent = (typeof CHANNEL_EVENTS)[keyof typeof CHANNEL_EVENTS];
|
||||
/**
|
||||
* CONSTANTS
|
||||
*/
|
||||
export type Transport = (typeof TRANSPORTS)[keyof typeof TRANSPORTS];
|
||||
/**
|
||||
* CONSTANTS
|
||||
*/
|
||||
export type XhrState = (typeof XHR_STATES)[keyof typeof XHR_STATES];
|
||||
/**
|
||||
* PRESENCE
|
||||
*/
|
||||
export type PresenceEvents = {
|
||||
state: string;
|
||||
diff: string;
|
||||
};
|
||||
/**
|
||||
* PRESENCE
|
||||
*/
|
||||
export type PresenceOnJoin = (key: string, currentPresence: PresenceState, newPresence: PresenceState) => void;
|
||||
/**
|
||||
* PRESENCE
|
||||
*/
|
||||
export type PresenceOnLeave = (key: string, currentPresence: PresenceState, leftPresence: PresenceState) => void;
|
||||
/**
|
||||
* PRESENCE
|
||||
*/
|
||||
export type PresenceOnSync = () => void;
|
||||
/**
|
||||
* PRESENCE
|
||||
*/
|
||||
export type PresenceDiff = ({
|
||||
joins: PresenceState;
|
||||
leaves: PresenceState;
|
||||
});
|
||||
/**
|
||||
* PRESENCE
|
||||
*/
|
||||
export type PresenceState = ({
|
||||
metas: {
|
||||
phx_ref?: string;
|
||||
phx_ref_prev?: string;
|
||||
[key: string]: any;
|
||||
}[];
|
||||
});
|
||||
/**
|
||||
* PRESENCE
|
||||
*/
|
||||
export type PresenceOptions = {
|
||||
events?: PresenceEvents | undefined;
|
||||
};
|
||||
/**
|
||||
* SERIALIZER
|
||||
*/
|
||||
export type Message<T> = ({
|
||||
join_ref?: string | null;
|
||||
ref?: string | null;
|
||||
event: string;
|
||||
topic: string;
|
||||
payload: T;
|
||||
});
|
||||
export type Encode<T> = (msg: Message<Record<string, any>>, callback: (result: ArrayBuffer | string) => T) => T;
|
||||
export type Decode<T> = (rawPayload: ArrayBuffer | string, callback: (msg: Message<unknown>) => T) => T;
|
||||
/**
|
||||
* SOCKET
|
||||
*/
|
||||
export type SocketTransport = (typeof WebSocket | typeof LongPoll);
|
||||
/**
|
||||
* SOCKET
|
||||
*/
|
||||
export type SocketOnOpen = () => void;
|
||||
/**
|
||||
* SOCKET
|
||||
*/
|
||||
export type SocketOnClose = (event: CloseEvent) => void;
|
||||
/**
|
||||
* SOCKET
|
||||
*/
|
||||
export type SocketOnError = (error: Event, transportBefore: SocketTransport, establishedBefore: number) => void;
|
||||
/**
|
||||
* SOCKET
|
||||
*/
|
||||
export type SocketOnMessage = (rawMessage: Message<unknown>) => void;
|
||||
/**
|
||||
* SOCKET
|
||||
*/
|
||||
export type SocketStateChangeCallbacks = ({
|
||||
open: [string, SocketOnOpen][];
|
||||
close: [string, SocketOnClose][];
|
||||
error: [string, SocketOnError][];
|
||||
message: [string, SocketOnMessage][];
|
||||
});
|
||||
/**
|
||||
* SOCKET
|
||||
*/
|
||||
export type HeartbeatStatus = "sent" | "ok" | "error" | "timeout" | "disconnected";
|
||||
/**
|
||||
* SOCKET
|
||||
*/
|
||||
export type HeartbeatCallback = (status: HeartbeatStatus, latency?: number) => void;
|
||||
/**
|
||||
* SOCKET
|
||||
*/
|
||||
export type SocketOptions = {
|
||||
/**
|
||||
* - The Websocket Transport, for example WebSocket or Phoenix.LongPoll.
|
||||
*/
|
||||
transport?: SocketTransport | undefined;
|
||||
/**
|
||||
* - The millisecond time to attempt the primary transport
|
||||
* before falling back to the LongPoll transport. Disabled by default.
|
||||
*/
|
||||
longPollFallbackMs?: number | undefined;
|
||||
/**
|
||||
* - The millisecond time before LongPoll transport times out. Default 20000.
|
||||
*/
|
||||
longpollerTimeout?: number | undefined;
|
||||
/**
|
||||
* - When true, enables debug logging. Default false.
|
||||
*/
|
||||
debug?: boolean | undefined;
|
||||
/**
|
||||
* - The function to encode outgoing messages.
|
||||
* Defaults to JSON encoder.
|
||||
*/
|
||||
encode?: Encode<void> | undefined;
|
||||
/**
|
||||
* - The function to decode incoming messages.
|
||||
* Defaults to JSON:
|
||||
*
|
||||
* ```javascript
|
||||
* (payload, callback) => callback(JSON.parse(payload))
|
||||
* ```
|
||||
*/
|
||||
decode?: Decode<void> | undefined;
|
||||
/**
|
||||
* - The default timeout in milliseconds to trigger push timeouts.
|
||||
* Defaults `DEFAULT_TIMEOUT`
|
||||
*/
|
||||
timeout?: number | undefined;
|
||||
/**
|
||||
* - The millisec interval to send a heartbeat message
|
||||
*/
|
||||
heartbeatIntervalMs?: number | undefined;
|
||||
/**
|
||||
* - Whether to automatically send heartbeats after
|
||||
* connection is established.
|
||||
*
|
||||
* Defaults to true.
|
||||
*/
|
||||
autoSendHeartbeat?: boolean | undefined;
|
||||
/**
|
||||
* - The optional function to handle heartbeat status and latency.
|
||||
*/
|
||||
heartbeatCallback?: HeartbeatCallback | undefined;
|
||||
/**
|
||||
* - The optional function that returns the
|
||||
* socket reconnect interval, in milliseconds.
|
||||
*
|
||||
* Defaults to stepped backoff of:
|
||||
*
|
||||
* ```javascript
|
||||
* function(tries){
|
||||
* return [10, 50, 100, 150, 200, 250, 500, 1000, 2000][tries - 1] || 5000
|
||||
* }
|
||||
* ````
|
||||
*/
|
||||
reconnectAfterMs?: ((tries: number) => number) | undefined;
|
||||
/**
|
||||
* - The optional function that returns the millisec
|
||||
* rejoin interval for individual channels.
|
||||
*
|
||||
* ```javascript
|
||||
* function(tries){
|
||||
* return [1000, 2000, 5000][tries - 1] || 10000
|
||||
* }
|
||||
* ````
|
||||
*/
|
||||
rejoinAfterMs?: ((tries: number) => number) | undefined;
|
||||
/**
|
||||
* - The optional function for specialized logging, ie:
|
||||
*
|
||||
* ```javascript
|
||||
* function(kind, msg, data) {
|
||||
* console.log(`${kind}: ${msg}`, data)
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
logger?: ((kind: string, msg: string, data: any) => void) | undefined;
|
||||
/**
|
||||
* - The optional params to pass when connecting
|
||||
*/
|
||||
params?: Closure<Params> | undefined;
|
||||
/**
|
||||
* - the optional authentication token to be exposed on the server
|
||||
* under the `:auth_token` connect_info key. Can be a string or a function that returns a string.
|
||||
*/
|
||||
authToken?: Closure<string> | undefined;
|
||||
/**
|
||||
* - The binary type to use for binary WebSocket frames.
|
||||
*
|
||||
* Defaults to "arraybuffer"
|
||||
*/
|
||||
binaryType?: BinaryType | undefined;
|
||||
/**
|
||||
* - The serializer's protocol version to send on connect.
|
||||
*
|
||||
* Defaults to DEFAULT_VSN.
|
||||
*/
|
||||
vsn?: Vsn | undefined;
|
||||
/**
|
||||
* - An optional Storage compatible object
|
||||
* Phoenix uses sessionStorage for longpoll fallback history. Overriding the store is
|
||||
* useful when Phoenix won't have access to `sessionStorage`. For example, This could
|
||||
* happen if a site loads a cross-domain channel in an iframe. Example usage:
|
||||
*
|
||||
* class InMemoryStorage {
|
||||
* constructor() { this.storage = {} }
|
||||
* getItem(keyName) { return this.storage[keyName] || null }
|
||||
* removeItem(keyName) { delete this.storage[keyName] }
|
||||
* setItem(keyName, keyValue) { this.storage[keyName] = keyValue }
|
||||
* }
|
||||
*/
|
||||
sessionStorage?: Storage | undefined;
|
||||
/**
|
||||
* - Callback ran before socket tries to reconnect.
|
||||
*/
|
||||
beforeReconnect?: (() => Promise<void>) | undefined;
|
||||
};
|
||||
import type { SOCKET_STATES } from "./constants";
|
||||
import type { CHANNEL_STATES } from "./constants";
|
||||
import type { CHANNEL_EVENTS } from "./constants";
|
||||
import type { TRANSPORTS } from "./constants";
|
||||
import type { XHR_STATES } from "./constants";
|
||||
import type LongPoll from "./longpoll";
|
||||
//# sourceMappingURL=types.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../assets/js/phoenix/types.js"],"names":[],"mappings":";;;qBAMa,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;oBAIvB,CAAC,IACD,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;;;;qCAMb,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI;;;;qCAC3E,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI;;;;6BACzB,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,sBAAsB,CAAA;CAAC,CAAC;;;;+BAChE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,EAAG,MAAM,OAAA,EAAE,OAAO,CAAC,EAAG,MAAM,OAAA,KAAK,OAAO;;;;oCAC/E,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,EAAG,MAAM,OAAA,KAAK,OAAO;;;;kBAOrE,OAAO,GAAG,OAAO;;;;0BACjB,CAAO,oBAAa,EAAC,MAAa,oBAAa,CAAC;;;;2BAChD,CAAO,qBAAc,EAAC,MAAa,qBAAc,CAAC;;;;2BAClD,CAAO,qBAAc,EAAC,MAAa,qBAAc,CAAC;;;;wBAClD,CAAO,iBAAU,EAAC,MAAa,iBAAU,CAAC;;;;uBAC1C,CAAO,iBAAU,EAAC,MAAa,iBAAU,CAAC;;;;6BAK1C;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC;;;;6BAC7B,CAAC,GAAG,EAAE,MAAM,EAAE,eAAe,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,KAAK,IAAI;;;;8BACjF,CAAC,GAAG,EAAE,MAAM,EAAE,eAAe,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,KAAK,IAAI;;;;6BAClF,MAAM,IAAI;;;;2BACV,CAAC;IAAC,KAAK,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,aAAa,CAAA;CAAC,CAAC;;;;4BAC/C,CACZ;IACK,KAAK,EAAE;QACL,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,YAAY,CAAC,EAAE,MAAM,CAAA;QAC5B,CAAQ,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KACnB,EAAE,CAAA;CACJ,CACF;;;;;;;;;;oBAQU,CAAC,IACD,CAAC;IACX,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,CAAC,CAAC;CACV,CAAC;mBAGQ,CAAC,IACD,CAAC,GAAG,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,KAAK,CAAC,KAAK,CAAC;mBAGvF,CAAC,IACD,CAAC,UAAU,EAAE,WAAW,GAAG,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC;;;;8BAK/E,CAAC,OAAO,SAAS,GAAG,eAAe,CAAC;;;;2BACpC,MAAM,IAAI;;;;4BACV,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI;;;;4BAC3B,CAAC,KAAK,EAAE,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,KAAK,IAAI;;;;8BACnF,CAAC,UAAU,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI;;;;yCACtC,CAAC;IACT,IAAI,EAAE,CAAC,MAAM,EAAE,YAAY,CAAC,EAAE,CAAA;IAC9B,KAAK,EAAE,CAAC,MAAM,EAAE,aAAa,CAAC,EAAE,CAAA;IAChC,KAAK,EAAE,CAAC,MAAM,EAAE,aAAa,CAAC,EAAE,CAAA;IAChC,OAAO,EAAE,CAAC,MAAM,EAAE,eAAe,CAAC,EAAE,CAAA;CACrC,CAAC;;;;8BACQ,MAAM,GAAG,IAAI,GAAG,OAAO,GAAG,SAAS,GAAG,cAAc;;;;gCACpD,CAAC,MAAM,EAAE,eAAe,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAoC1C,MAAM,KAAK,MAAM;;;;;;;;;;;6BAWjB,MAAM,KAAK,MAAM;;;;;;;;;;qBASlB,MAAM,OAAO,MAAM,QAAQ,GAAG,KAAK,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6BAiCxC,OAAO,CAAC,IAAI,CAAC;;mCAzJuD,aAAa;oCAAb,aAAa;oCAAb,aAAa;gCAAb,aAAa;gCAAb,aAAa;0BA1B5E,YAAY"}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export function closure<T>(value: T | (() => T)): () => T;
|
||||
//# sourceMappingURL=utils.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../assets/js/phoenix/utils.js"],"names":[],"mappings":"AAQO,wBAJK,CAAC,SACH,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GACX,MAAM,CAAC,CASlB"}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"include": [
|
||||
"assets/js/phoenix/**/*.js"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
"module": "ESNext", /* Specify what module code is generated. */
|
||||
"moduleResolution": "bundler", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
"rootDir": "./assets/js/phoenix", /* Specify the root folder within your source files. */
|
||||
"allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
"declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
"emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
"outDir": "./priv/static/types", /* Specify an output folder for all emitted files. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user