This commit is contained in:
MarcWieland
2026-07-26 23:57:23 +02:00
commit 8b613ec0bc
6300 changed files with 1257646 additions and 0 deletions
+22
View File
@@ -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
View File
@@ -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.
[![Build Status](https://github.com/supabase/phoenix/workflows/CI/badge.svg)](https://github.com/supabase/phoenix/actions/workflows/ci.yml) [![npm version](https://img.shields.io/npm/v/@supabase/phoenix.svg)](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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+10
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1 @@
{"version":3,"file":"socket.d.ts","sourceRoot":"","sources":["../../../assets/js/phoenix/socket.js"],"names":[],"mappings":"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"}
+36
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1 @@
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../assets/js/phoenix/types.js"],"names":[],"mappings":"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"}
+2
View File
@@ -0,0 +1,2 @@
export function closure<T>(value: T | (() => T)): () => T;
//# sourceMappingURL=utils.d.ts.map
+1
View File
@@ -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
View File
@@ -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. */
}
}