@supabase/phoenix 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +22 -0
- package/README.md +122 -0
- package/assets/js/phoenix/ajax.js +116 -0
- package/assets/js/phoenix/channel.js +331 -0
- package/assets/js/phoenix/constants.js +35 -0
- package/assets/js/phoenix/index.js +212 -0
- package/assets/js/phoenix/longpoll.js +192 -0
- package/assets/js/phoenix/presence.js +208 -0
- package/assets/js/phoenix/push.js +134 -0
- package/assets/js/phoenix/serializer.js +133 -0
- package/assets/js/phoenix/socket.js +747 -0
- package/assets/js/phoenix/timer.js +48 -0
- package/assets/js/phoenix/types.js +184 -0
- package/assets/js/phoenix/utils.js +16 -0
- package/package.json +58 -0
- package/priv/static/favicon.ico +0 -0
- package/priv/static/phoenix-orange.png +0 -0
- package/priv/static/phoenix.cjs.js +1812 -0
- package/priv/static/phoenix.cjs.js.map +7 -0
- package/priv/static/phoenix.js +1834 -0
- package/priv/static/phoenix.min.js +2 -0
- package/priv/static/phoenix.mjs +1789 -0
- package/priv/static/phoenix.mjs.map +7 -0
- package/priv/static/phoenix.png +0 -0
- package/priv/static/types/ajax.d.ts +10 -0
- package/priv/static/types/ajax.d.ts.map +1 -0
- package/priv/static/types/channel.d.ts +167 -0
- package/priv/static/types/channel.d.ts.map +1 -0
- package/priv/static/types/constants.d.ts +36 -0
- package/priv/static/types/constants.d.ts.map +1 -0
- package/priv/static/types/index.d.ts +10 -0
- package/priv/static/types/index.d.ts.map +1 -0
- package/priv/static/types/longpoll.d.ts +29 -0
- package/priv/static/types/longpoll.d.ts.map +1 -0
- package/priv/static/types/presence.d.ts +107 -0
- package/priv/static/types/presence.d.ts.map +1 -0
- package/priv/static/types/push.d.ts +70 -0
- package/priv/static/types/push.d.ts.map +1 -0
- package/priv/static/types/serializer.d.ts +74 -0
- package/priv/static/types/serializer.d.ts.map +1 -0
- package/priv/static/types/socket.d.ts +284 -0
- package/priv/static/types/socket.d.ts.map +1 -0
- package/priv/static/types/timer.d.ts +36 -0
- package/priv/static/types/timer.d.ts.map +1 -0
- package/priv/static/types/types.d.ts +280 -0
- package/priv/static/types/types.d.ts.map +1 -0
- package/priv/static/types/utils.d.ts +2 -0
- package/priv/static/types/utils.d.ts.map +1 -0
- package/tsconfig.json +20 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2014 Chris McCord
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<picture>
|
|
2
|
+
<source media="(prefers-color-scheme: dark)" srcset="./priv/static/phoenix-orange.png" />
|
|
3
|
+
<source media="(prefers-color-scheme: light)" srcset="./priv/static/phoenix.png" />
|
|
4
|
+
<img src="./priv/static/phoenix.png" alt="Phoenix logo" />
|
|
5
|
+
</picture>
|
|
6
|
+
|
|
7
|
+
> Peace of mind from prototype to production.
|
|
8
|
+
|
|
9
|
+
[](https://github.com/supabase/phoenix/actions/workflows/ci.yml) [](https://www.npmjs.com/package/@supabase/phoenix)
|
|
10
|
+
|
|
11
|
+
## Supabase Fork
|
|
12
|
+
|
|
13
|
+
This is a Supabase fork of Phoenix Framework, published to npm as `@supabase/phoenix`.
|
|
14
|
+
|
|
15
|
+
**Installation:**
|
|
16
|
+
```bash
|
|
17
|
+
npm install @supabase/phoenix
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Releases**: This fork uses automated releases via [release-please](https://github.com/googleapis/release-please). See [RELEASE.md](RELEASE.md) for details.
|
|
21
|
+
|
|
22
|
+
**Upstream**: Based on [phoenixframework/phoenix](https://github.com/phoenixframework/phoenix)
|
|
23
|
+
|
|
24
|
+
## Versioning
|
|
25
|
+
|
|
26
|
+
This package uses **independent semantic versioning** for the JavaScript client.
|
|
27
|
+
|
|
28
|
+
- **Current version**: 0.1.0
|
|
29
|
+
- **Based on**: Phoenix Framework 1.8.3 JS client
|
|
30
|
+
- **Last synced**: 2026-02-17
|
|
31
|
+
|
|
32
|
+
We version based on **JS API changes only**, not upstream Phoenix framework releases.
|
|
33
|
+
When we merge upstream Phoenix changes, we evaluate the JS API impact and version accordingly.
|
|
34
|
+
|
|
35
|
+
## Getting started
|
|
36
|
+
|
|
37
|
+
See the official site at <https://www.phoenixframework.org/>.
|
|
38
|
+
|
|
39
|
+
Install the latest version of Phoenix by following the instructions at <https://hexdocs.pm/phoenix/installation.html#phoenix>.
|
|
40
|
+
|
|
41
|
+
## Documentation
|
|
42
|
+
|
|
43
|
+
API documentation is available at <https://hexdocs.pm/phoenix>.
|
|
44
|
+
|
|
45
|
+
Phoenix.js documentation is available at <https://hexdocs.pm/phoenix/js>.
|
|
46
|
+
|
|
47
|
+
## Contributing
|
|
48
|
+
|
|
49
|
+
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].
|
|
50
|
+
|
|
51
|
+
### Generating a Phoenix project from unreleased versions
|
|
52
|
+
|
|
53
|
+
You can create a new project using the latest Phoenix source installer (the `phx.new` Mix task) with the following steps:
|
|
54
|
+
|
|
55
|
+
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/`.
|
|
56
|
+
2. Copy this repo via `git clone https://github.com/phoenixframework/phoenix` or by downloading it
|
|
57
|
+
3. Run the `phx.new` Mix task from within the `installer` directory, for example:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
cd phoenix/installer
|
|
61
|
+
mix phx.new dev_app --dev
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The `--dev` flag will configure your new project's `:phoenix` dep as a relative path dependency, pointing to your local Phoenix checkout:
|
|
65
|
+
|
|
66
|
+
```elixir
|
|
67
|
+
defp deps do
|
|
68
|
+
[{:phoenix, path: "../..", override: true},
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
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)
|
|
72
|
+
|
|
73
|
+
### Building from source
|
|
74
|
+
|
|
75
|
+
To build the documentation:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm install
|
|
79
|
+
MIX_ENV=docs mix docs
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
To build Phoenix:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
mix deps.get
|
|
86
|
+
mix compile
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
To build the Phoenix installer:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
mix deps.get
|
|
93
|
+
mix compile
|
|
94
|
+
mix archive.build
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
To build Phoenix.js:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
cd assets
|
|
101
|
+
npm install
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Important links
|
|
105
|
+
|
|
106
|
+
* [#elixir][1] on [Libera][2] IRC
|
|
107
|
+
* [elixir-lang Slack channel][3]
|
|
108
|
+
* [Issues tracker][4]
|
|
109
|
+
* [Phoenix Forum (questions and proposals)][5]
|
|
110
|
+
* Visit Phoenix's sponsor, DockYard, for expert [Phoenix Consulting](https://dockyard.com/phoenix-consulting)
|
|
111
|
+
|
|
112
|
+
[1]: https://web.libera.chat/?channels=#elixir
|
|
113
|
+
[2]: https://libera.chat/
|
|
114
|
+
[3]: https://elixir-lang.slack.com/
|
|
115
|
+
[4]: https://github.com/phoenixframework/phoenix/issues
|
|
116
|
+
[5]: https://elixirforum.com/c/phoenix-forum
|
|
117
|
+
|
|
118
|
+
## Copyright and License
|
|
119
|
+
|
|
120
|
+
Copyright (c) 2014, Chris McCord.
|
|
121
|
+
|
|
122
|
+
Phoenix source code is licensed under the [MIT License](LICENSE.md).
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
global,
|
|
3
|
+
XHR_STATES
|
|
4
|
+
} from "./constants"
|
|
5
|
+
|
|
6
|
+
export default class Ajax {
|
|
7
|
+
|
|
8
|
+
static request(method, endPoint, headers, body, timeout, ontimeout, callback){
|
|
9
|
+
if(global.XDomainRequest){
|
|
10
|
+
let req = new global.XDomainRequest() // IE8, IE9
|
|
11
|
+
return this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback)
|
|
12
|
+
} else if(global.XMLHttpRequest){
|
|
13
|
+
let req = new global.XMLHttpRequest() // IE7+, Firefox, Chrome, Opera, Safari
|
|
14
|
+
return this.xhrRequest(req, method, endPoint, headers, body, timeout, ontimeout, callback)
|
|
15
|
+
} else if(global.fetch && global.AbortController){
|
|
16
|
+
// Fetch with AbortController for modern browsers
|
|
17
|
+
return this.fetchRequest(method, endPoint, headers, body, timeout, ontimeout, callback)
|
|
18
|
+
} else {
|
|
19
|
+
throw new Error("No suitable XMLHttpRequest implementation found")
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static fetchRequest(method, endPoint, headers, body, timeout, ontimeout, callback){
|
|
24
|
+
let options = {
|
|
25
|
+
method,
|
|
26
|
+
headers,
|
|
27
|
+
body,
|
|
28
|
+
}
|
|
29
|
+
let controller = null
|
|
30
|
+
if(timeout){
|
|
31
|
+
controller = new AbortController()
|
|
32
|
+
const _timeoutId = setTimeout(() => controller.abort(), timeout)
|
|
33
|
+
options.signal = controller.signal
|
|
34
|
+
}
|
|
35
|
+
global.fetch(endPoint, options)
|
|
36
|
+
.then(response => response.text())
|
|
37
|
+
.then(data => this.parseJSON(data))
|
|
38
|
+
.then(data => callback && callback(data))
|
|
39
|
+
.catch(err => {
|
|
40
|
+
if(err.name === "AbortError" && ontimeout){
|
|
41
|
+
ontimeout()
|
|
42
|
+
} else {
|
|
43
|
+
callback && callback(null)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
return controller
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback){
|
|
50
|
+
req.timeout = timeout
|
|
51
|
+
req.open(method, endPoint)
|
|
52
|
+
req.onload = () => {
|
|
53
|
+
let response = this.parseJSON(req.responseText)
|
|
54
|
+
callback && callback(response)
|
|
55
|
+
}
|
|
56
|
+
if(ontimeout){ req.ontimeout = ontimeout }
|
|
57
|
+
|
|
58
|
+
// Work around bug in IE9 that requires an attached onprogress handler
|
|
59
|
+
req.onprogress = () => { }
|
|
60
|
+
|
|
61
|
+
req.send(body)
|
|
62
|
+
return req
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
static xhrRequest(req, method, endPoint, headers, body, timeout, ontimeout, callback){
|
|
66
|
+
req.open(method, endPoint, true)
|
|
67
|
+
req.timeout = timeout
|
|
68
|
+
for(let [key, value] of Object.entries(headers)){
|
|
69
|
+
req.setRequestHeader(key, value)
|
|
70
|
+
}
|
|
71
|
+
req.onerror = () => callback && callback(null)
|
|
72
|
+
req.onreadystatechange = () => {
|
|
73
|
+
if(req.readyState === XHR_STATES.complete && callback){
|
|
74
|
+
let response = this.parseJSON(req.responseText)
|
|
75
|
+
callback(response)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if(ontimeout){ req.ontimeout = ontimeout }
|
|
79
|
+
|
|
80
|
+
req.send(body)
|
|
81
|
+
return req
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static parseJSON(resp){
|
|
85
|
+
if(!resp || resp === ""){ return null }
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(resp)
|
|
89
|
+
} catch {
|
|
90
|
+
console && console.log("failed to parse JSON response", resp)
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
static serialize(obj, parentKey){
|
|
96
|
+
let queryStr = []
|
|
97
|
+
for(var key in obj){
|
|
98
|
+
if(!Object.prototype.hasOwnProperty.call(obj, key)){ continue }
|
|
99
|
+
let paramKey = parentKey ? `${parentKey}[${key}]` : key
|
|
100
|
+
let paramVal = obj[key]
|
|
101
|
+
if(typeof paramVal === "object"){
|
|
102
|
+
queryStr.push(this.serialize(paramVal, paramKey))
|
|
103
|
+
} else {
|
|
104
|
+
queryStr.push(encodeURIComponent(paramKey) + "=" + encodeURIComponent(paramVal))
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return queryStr.join("&")
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
static appendParams(url, params){
|
|
111
|
+
if(Object.keys(params).length === 0){ return url }
|
|
112
|
+
|
|
113
|
+
let prefix = url.match(/\?/) ? "&" : "?"
|
|
114
|
+
return `${url}${prefix}${this.serialize(params)}`
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import {closure} from "./utils"
|
|
2
|
+
import {
|
|
3
|
+
CHANNEL_EVENTS,
|
|
4
|
+
CHANNEL_STATES,
|
|
5
|
+
} from "./constants"
|
|
6
|
+
|
|
7
|
+
import Push from "./push"
|
|
8
|
+
import Timer from "./timer"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @import Socket from "./socket"
|
|
12
|
+
* @import { ChannelState, Params, ChannelBindingCallback, ChannelOnMessage, ChannelFilterBindings, ChannelOnErrorCallback, ChannelBinding } from "./types"
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export default class Channel {
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} topic
|
|
18
|
+
* @param {Params | (() => Params)} params
|
|
19
|
+
* @param {Socket} socket
|
|
20
|
+
*/
|
|
21
|
+
constructor(topic, params, socket){
|
|
22
|
+
/** @type{ChannelState} */
|
|
23
|
+
this.state = CHANNEL_STATES.closed
|
|
24
|
+
/** @type{string} */
|
|
25
|
+
this.topic = topic
|
|
26
|
+
/** @type{() => Params} */
|
|
27
|
+
this.params = closure(params || {})
|
|
28
|
+
/** @type {Socket} */
|
|
29
|
+
this.socket = socket
|
|
30
|
+
/** @type{ChannelBinding[]} */
|
|
31
|
+
this.bindings = []
|
|
32
|
+
/** @type{number} */
|
|
33
|
+
this.bindingRef = 0
|
|
34
|
+
/** @type{number} */
|
|
35
|
+
this.timeout = this.socket.timeout
|
|
36
|
+
/** @type{boolean} */
|
|
37
|
+
this.joinedOnce = false
|
|
38
|
+
/** @type{Push} */
|
|
39
|
+
this.joinPush = new Push(this, CHANNEL_EVENTS.join, this.params, this.timeout)
|
|
40
|
+
/** @type{Push[]} */
|
|
41
|
+
this.pushBuffer = []
|
|
42
|
+
/** @type{string[]} */
|
|
43
|
+
this.stateChangeRefs = []
|
|
44
|
+
|
|
45
|
+
/** @type{Timer} */
|
|
46
|
+
this.rejoinTimer = new Timer(() => {
|
|
47
|
+
if(this.socket.isConnected()){ this.rejoin() }
|
|
48
|
+
}, this.socket.rejoinAfterMs)
|
|
49
|
+
this.stateChangeRefs.push(this.socket.onError(() => this.rejoinTimer.reset()))
|
|
50
|
+
this.stateChangeRefs.push(this.socket.onOpen(() => {
|
|
51
|
+
this.rejoinTimer.reset()
|
|
52
|
+
if(this.isErrored()){ this.rejoin() }
|
|
53
|
+
})
|
|
54
|
+
)
|
|
55
|
+
this.joinPush.receive("ok", () => {
|
|
56
|
+
this.state = CHANNEL_STATES.joined
|
|
57
|
+
this.rejoinTimer.reset()
|
|
58
|
+
this.pushBuffer.forEach(pushEvent => pushEvent.send())
|
|
59
|
+
this.pushBuffer = []
|
|
60
|
+
})
|
|
61
|
+
this.joinPush.receive("error", (reason) => {
|
|
62
|
+
this.state = CHANNEL_STATES.errored
|
|
63
|
+
if(this.socket.hasLogger()) this.socket.log("channel", `error ${this.topic}`, reason)
|
|
64
|
+
if(this.socket.isConnected()){ this.rejoinTimer.scheduleTimeout() }
|
|
65
|
+
})
|
|
66
|
+
this.onClose(() => {
|
|
67
|
+
this.rejoinTimer.reset()
|
|
68
|
+
if(this.socket.hasLogger()) this.socket.log("channel", `close ${this.topic}`)
|
|
69
|
+
this.state = CHANNEL_STATES.closed
|
|
70
|
+
this.socket.remove(this)
|
|
71
|
+
})
|
|
72
|
+
this.onError(reason => {
|
|
73
|
+
if(this.socket.hasLogger()) this.socket.log("channel", `error ${this.topic}`, reason)
|
|
74
|
+
if(this.isJoining()){ this.joinPush.reset() }
|
|
75
|
+
this.state = CHANNEL_STATES.errored
|
|
76
|
+
if(this.socket.isConnected()){ this.rejoinTimer.scheduleTimeout() }
|
|
77
|
+
})
|
|
78
|
+
this.joinPush.receive("timeout", () => {
|
|
79
|
+
if(this.socket.hasLogger()) this.socket.log("channel", `timeout ${this.topic}`, this.joinPush.timeout)
|
|
80
|
+
let leavePush = new Push(this, CHANNEL_EVENTS.leave, closure({}), this.timeout)
|
|
81
|
+
leavePush.send()
|
|
82
|
+
this.state = CHANNEL_STATES.errored
|
|
83
|
+
this.joinPush.reset()
|
|
84
|
+
if(this.socket.isConnected()){ this.rejoinTimer.scheduleTimeout() }
|
|
85
|
+
})
|
|
86
|
+
this.on(CHANNEL_EVENTS.reply, (payload, ref) => {
|
|
87
|
+
this.trigger(this.replyEventName(ref), payload)
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Join the channel
|
|
93
|
+
* @param {number} timeout
|
|
94
|
+
* @returns {Push}
|
|
95
|
+
*/
|
|
96
|
+
join(timeout = this.timeout){
|
|
97
|
+
if(this.joinedOnce){
|
|
98
|
+
throw new Error("tried to join multiple times. 'join' can only be called a single time per channel instance")
|
|
99
|
+
} else {
|
|
100
|
+
this.timeout = timeout
|
|
101
|
+
this.joinedOnce = true
|
|
102
|
+
this.rejoin()
|
|
103
|
+
return this.joinPush
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Teardown the channel.
|
|
109
|
+
*
|
|
110
|
+
* Destroys and stops related timers.
|
|
111
|
+
*/
|
|
112
|
+
teardown(){
|
|
113
|
+
this.pushBuffer.forEach((push) => push.destroy())
|
|
114
|
+
this.pushBuffer = []
|
|
115
|
+
this.rejoinTimer.reset()
|
|
116
|
+
this.joinPush.destroy()
|
|
117
|
+
this.state = CHANNEL_STATES.closed
|
|
118
|
+
this.bindings = {}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Hook into channel close
|
|
123
|
+
* @param {ChannelBindingCallback} callback
|
|
124
|
+
*/
|
|
125
|
+
onClose(callback){
|
|
126
|
+
this.on(CHANNEL_EVENTS.close, callback)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Hook into channel errors
|
|
131
|
+
* @param {ChannelOnErrorCallback} callback
|
|
132
|
+
* @return {number}
|
|
133
|
+
*/
|
|
134
|
+
onError(callback){
|
|
135
|
+
return this.on(CHANNEL_EVENTS.error, reason => callback(reason))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Subscribes on channel events
|
|
140
|
+
*
|
|
141
|
+
* Subscription returns a ref counter, which can be used later to
|
|
142
|
+
* unsubscribe the exact event listener
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* const ref1 = channel.on("event", do_stuff)
|
|
146
|
+
* const ref2 = channel.on("event", do_other_stuff)
|
|
147
|
+
* channel.off("event", ref1)
|
|
148
|
+
* // Since unsubscription, do_stuff won't fire,
|
|
149
|
+
* // while do_other_stuff will keep firing on the "event"
|
|
150
|
+
*
|
|
151
|
+
* @param {string} event
|
|
152
|
+
* @param {ChannelBindingCallback} callback
|
|
153
|
+
* @returns {number} ref
|
|
154
|
+
*/
|
|
155
|
+
on(event, callback){
|
|
156
|
+
let ref = this.bindingRef++
|
|
157
|
+
this.bindings.push({event, ref, callback})
|
|
158
|
+
return ref
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Unsubscribes off of channel events
|
|
163
|
+
*
|
|
164
|
+
* Use the ref returned from a channel.on() to unsubscribe one
|
|
165
|
+
* handler, or pass nothing for the ref to unsubscribe all
|
|
166
|
+
* handlers for the given event.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* // Unsubscribe the do_stuff handler
|
|
170
|
+
* const ref1 = channel.on("event", do_stuff)
|
|
171
|
+
* channel.off("event", ref1)
|
|
172
|
+
*
|
|
173
|
+
* // Unsubscribe all handlers from event
|
|
174
|
+
* channel.off("event")
|
|
175
|
+
*
|
|
176
|
+
* @param {string} event
|
|
177
|
+
* @param {number} [ref]
|
|
178
|
+
*/
|
|
179
|
+
off(event, ref){
|
|
180
|
+
this.bindings = this.bindings.filter((bind) => {
|
|
181
|
+
return !(bind.event === event && (typeof ref === "undefined" || ref === bind.ref))
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @private
|
|
187
|
+
*/
|
|
188
|
+
canPush(){ return this.socket.isConnected() && this.isJoined() }
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Sends a message `event` to phoenix with the payload `payload`.
|
|
192
|
+
* Phoenix receives this in the `handle_in(event, payload, socket)`
|
|
193
|
+
* function. if phoenix replies or it times out (default 10000ms),
|
|
194
|
+
* then optionally the reply can be received.
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* channel.push("event")
|
|
198
|
+
* .receive("ok", payload => console.log("phoenix replied:", payload))
|
|
199
|
+
* .receive("error", err => console.log("phoenix errored", err))
|
|
200
|
+
* .receive("timeout", () => console.log("timed out pushing"))
|
|
201
|
+
* @param {string} event
|
|
202
|
+
* @param {Object} payload
|
|
203
|
+
* @param {number} [timeout]
|
|
204
|
+
* @returns {Push}
|
|
205
|
+
*/
|
|
206
|
+
push(event, payload, timeout = this.timeout){
|
|
207
|
+
payload = payload || {}
|
|
208
|
+
if(!this.joinedOnce){
|
|
209
|
+
throw new Error(`tried to push '${event}' to '${this.topic}' before joining. Use channel.join() before pushing events`)
|
|
210
|
+
}
|
|
211
|
+
let pushEvent = new Push(this, event, function (){ return payload }, timeout)
|
|
212
|
+
if(this.canPush()){
|
|
213
|
+
pushEvent.send()
|
|
214
|
+
} else {
|
|
215
|
+
pushEvent.startTimeout()
|
|
216
|
+
this.pushBuffer.push(pushEvent)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return pushEvent
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Leaves the channel
|
|
223
|
+
*
|
|
224
|
+
* Unsubscribes from server events, and
|
|
225
|
+
* instructs channel to terminate on server
|
|
226
|
+
*
|
|
227
|
+
* Triggers onClose() hooks
|
|
228
|
+
*
|
|
229
|
+
* To receive leave acknowledgements, use the `receive`
|
|
230
|
+
* hook to bind to the server ack, ie:
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* channel.leave().receive("ok", () => alert("left!") )
|
|
234
|
+
*
|
|
235
|
+
* @param {number} timeout
|
|
236
|
+
* @returns {Push}
|
|
237
|
+
*/
|
|
238
|
+
leave(timeout = this.timeout){
|
|
239
|
+
this.rejoinTimer.reset()
|
|
240
|
+
this.joinPush.cancelTimeout()
|
|
241
|
+
|
|
242
|
+
this.state = CHANNEL_STATES.leaving
|
|
243
|
+
let onClose = () => {
|
|
244
|
+
if(this.socket.hasLogger()) this.socket.log("channel", `leave ${this.topic}`)
|
|
245
|
+
this.trigger(CHANNEL_EVENTS.close, "leave")
|
|
246
|
+
}
|
|
247
|
+
let leavePush = new Push(this, CHANNEL_EVENTS.leave, closure({}), timeout)
|
|
248
|
+
leavePush.receive("ok", () => onClose())
|
|
249
|
+
.receive("timeout", () => onClose())
|
|
250
|
+
leavePush.send()
|
|
251
|
+
if(!this.canPush()){ leavePush.trigger("ok", {}) }
|
|
252
|
+
|
|
253
|
+
return leavePush
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Overridable message hook
|
|
258
|
+
*
|
|
259
|
+
* Receives all events for specialized message handling
|
|
260
|
+
* before dispatching to the channel callbacks.
|
|
261
|
+
*
|
|
262
|
+
* Must return the payload, modified or unmodified
|
|
263
|
+
* @type{ChannelOnMessage}
|
|
264
|
+
*/
|
|
265
|
+
onMessage(_event, payload, _ref){ return payload }
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Overridable filter hook
|
|
269
|
+
*
|
|
270
|
+
* If this function returns `true`, `binding`'s callback will be called.
|
|
271
|
+
*
|
|
272
|
+
* @type{ChannelFilterBindings}
|
|
273
|
+
*/
|
|
274
|
+
filterBindings(_binding, _payload, _ref){ return true }
|
|
275
|
+
|
|
276
|
+
isMember(topic, event, payload, joinRef){
|
|
277
|
+
if(this.topic !== topic){ return false }
|
|
278
|
+
|
|
279
|
+
if(joinRef && joinRef !== this.joinRef()){
|
|
280
|
+
if(this.socket.hasLogger()) this.socket.log("channel", "dropping outdated message", {topic, event, payload, joinRef})
|
|
281
|
+
return false
|
|
282
|
+
} else {
|
|
283
|
+
return true
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
joinRef(){ return this.joinPush.ref }
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* @private
|
|
291
|
+
*/
|
|
292
|
+
rejoin(timeout = this.timeout){
|
|
293
|
+
if(this.isLeaving()){ return }
|
|
294
|
+
this.socket.leaveOpenTopic(this.topic)
|
|
295
|
+
this.state = CHANNEL_STATES.joining
|
|
296
|
+
this.joinPush.resend(timeout)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* @param {string} event
|
|
301
|
+
* @param {unknown} [payload]
|
|
302
|
+
* @param {?string} [ref]
|
|
303
|
+
* @param {?string} [joinRef]
|
|
304
|
+
*/
|
|
305
|
+
trigger(event, payload, ref, joinRef){
|
|
306
|
+
let handledPayload = this.onMessage(event, payload, ref, joinRef)
|
|
307
|
+
if(payload && !handledPayload){ throw new Error("channel onMessage callbacks must return the payload, modified or unmodified") }
|
|
308
|
+
|
|
309
|
+
let eventBindings = this.bindings.filter(bind => bind.event === event && this.filterBindings(bind, payload, ref))
|
|
310
|
+
|
|
311
|
+
for(let i = 0; i < eventBindings.length; i++){
|
|
312
|
+
let bind = eventBindings[i]
|
|
313
|
+
bind.callback(handledPayload, ref, joinRef || this.joinRef())
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* @param {string} ref
|
|
319
|
+
*/
|
|
320
|
+
replyEventName(ref){ return `chan_reply_${ref}` }
|
|
321
|
+
|
|
322
|
+
isClosed(){ return this.state === CHANNEL_STATES.closed }
|
|
323
|
+
|
|
324
|
+
isErrored(){ return this.state === CHANNEL_STATES.errored }
|
|
325
|
+
|
|
326
|
+
isJoined(){ return this.state === CHANNEL_STATES.joined }
|
|
327
|
+
|
|
328
|
+
isJoining(){ return this.state === CHANNEL_STATES.joining }
|
|
329
|
+
|
|
330
|
+
isLeaving(){ return this.state === CHANNEL_STATES.leaving }
|
|
331
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const globalSelf = typeof self !== "undefined" ? self : null
|
|
2
|
+
export const phxWindow = typeof window !== "undefined" ? window : null
|
|
3
|
+
export const global = globalSelf || phxWindow || globalThis
|
|
4
|
+
export const DEFAULT_VSN = "2.0.0"
|
|
5
|
+
export const DEFAULT_TIMEOUT = 10000
|
|
6
|
+
export const WS_CLOSE_NORMAL = 1000
|
|
7
|
+
|
|
8
|
+
export const SOCKET_STATES = /** @type {const} */ ({connecting: 0, open: 1, closing: 2, closed: 3})
|
|
9
|
+
|
|
10
|
+
export const CHANNEL_STATES = /** @type {const} */ ({
|
|
11
|
+
closed: "closed",
|
|
12
|
+
errored: "errored",
|
|
13
|
+
joined: "joined",
|
|
14
|
+
joining: "joining",
|
|
15
|
+
leaving: "leaving",
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export const CHANNEL_EVENTS = /** @type {const} */ ({
|
|
19
|
+
close: "phx_close",
|
|
20
|
+
error: "phx_error",
|
|
21
|
+
join: "phx_join",
|
|
22
|
+
reply: "phx_reply",
|
|
23
|
+
leave: "phx_leave"
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export const TRANSPORTS = /** @type {const} */ ({
|
|
27
|
+
longpoll: "longpoll",
|
|
28
|
+
websocket: "websocket"
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
export const XHR_STATES = /** @type {const} */ ({
|
|
32
|
+
complete: 4
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
export const AUTH_TOKEN_PREFIX = "base64url.bearer.phx."
|