@tokioapp/overlay 0.0.0-stage → 1.0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Italo Alfaro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,276 @@
1
- # Temporary Holding Version
1
+ # @tokioapp/overlay
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Receive TikTok LIVE events from [Tokio](https://tokio.ialfaro.com) in your stream overlays.
4
+
5
+ Tokio connects to a TikTok LIVE and sends its events (chat, gifts, likes, follows, music requests and more) to overlays over a local WebSocket. This library connects to it, reconnects when Tokio restarts, and hands you each event with typed data. It also receives the events Tokio's previews post to the overlay's iframe, so every overlay you import into Tokio needs it.
6
+
7
+ It has no dependencies and ships an ES module, a CommonJS build and a browser build, with TypeScript types. Formerly `tokio-event-loader`.
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm install @tokioapp/overlay
13
+ ```
14
+
15
+ Or load the browser build from a CDN. It defines a global `TokioOverlay`:
16
+
17
+ ```html
18
+ <script src="https://unpkg.com/@tokioapp/overlay@1"></script>
19
+ ```
20
+
21
+ ## The setup block
22
+
23
+ Your overlay's `index.html` must contain a `<script id="tokio-setup">` block. Write it yourself, before the scripts that read it:
24
+
25
+ ```html
26
+ <script id="tokio-setup">
27
+ window.__OVERLAY_ID__ = "";
28
+ window.__OVERLAY_CONFIG__ = { title: "Likes" };
29
+ </script>
30
+ ```
31
+
32
+ - `window.__OVERLAY_ID__` is required. Pass it to `tokio.connect`.
33
+ - `window.__OVERLAY_CONFIG__` is only for overlays with settings (`config: true` in `info.yaml`). Give it your `config.yaml` defaults.
34
+
35
+ Tokio rewrites the block with the overlay's ID and saved settings when it imports the overlay, and again when the streamer saves its settings. The values you write are what the page uses outside Tokio. Tokio refuses to import an overlay without the block.
36
+
37
+ If you build the overlay with a framework, keep the block in the `index.html` template so it survives the build. See [Building an overlay](https://tokio.ialfaro.com/docs/developers/building-an-overlay/) for the folder layout, `info.yaml` and `config.yaml`.
38
+
39
+ ## Usage
40
+
41
+ ```js
42
+ import { tokio, TOKIO_EVENTS } from "@tokioapp/overlay";
43
+
44
+ tokio.on(TOKIO_EVENTS.LIKE, (data) => {
45
+ console.log(`${data.user.username} liked, ${data.totalLikes} likes in total`);
46
+ });
47
+
48
+ tokio.connect(window.__OVERLAY_ID__);
49
+ ```
50
+
51
+ ### In a plain HTML page
52
+
53
+ ```html
54
+ <!doctype html>
55
+ <html lang="en">
56
+ <head>
57
+ <meta charset="UTF-8" />
58
+ <script src="https://unpkg.com/@tokioapp/overlay@1"></script>
59
+ <script id="tokio-setup">
60
+ window.__OVERLAY_ID__ = "";
61
+ window.__OVERLAY_CONFIG__ = { title: "Likes" };
62
+ </script>
63
+ </head>
64
+ <body>
65
+ <h1 id="title">Likes</h1>
66
+ <p id="likes">0</p>
67
+
68
+ <script>
69
+ const { tokio, TOKIO_EVENTS } = TokioOverlay;
70
+ const title = document.getElementById("title");
71
+ const likes = document.getElementById("likes");
72
+
73
+ const apply = (config) => {
74
+ title.textContent = config.title;
75
+ };
76
+
77
+ apply(window.__OVERLAY_CONFIG__);
78
+ // Sent when the streamer saves the overlay's settings.
79
+ tokio.on(TOKIO_EVENTS.CONFIG, apply);
80
+
81
+ tokio.on(TOKIO_EVENTS.LIKE, (data) => {
82
+ likes.textContent = data.totalLikes;
83
+ });
84
+
85
+ tokio.connect(window.__OVERLAY_ID__);
86
+ </script>
87
+ </body>
88
+ </html>
89
+ ```
90
+
91
+ ### In React
92
+
93
+ `tokio.on` returns its own unsubscribe function, so it fits an effect's cleanup:
94
+
95
+ ```jsx
96
+ useEffect(
97
+ () => tokio.on(TOKIO_EVENTS.LIKE, (data) => setLikes(data.totalLikes)),
98
+ [],
99
+ );
100
+ ```
101
+
102
+ Call `tokio.connect` once, when the overlay starts, not in each component.
103
+
104
+ ### Testing while you build
105
+
106
+ Before you import the overlay, its ID is still empty. To test it:
107
+
108
+ 1. In Tokio, turn on **Settings → Overlay Server**.
109
+ 2. Create an ID under **Tools → Test Overlays**.
110
+ 3. Connect with it, for example `tokio.connect("<test ID>")`, and open the overlay in your browser.
111
+ 4. Click **Test**. Tokio sends your overlay a sample of every LIVE event.
112
+
113
+ Switch back to `window.__OVERLAY_ID__` before you import it.
114
+
115
+ ## API
116
+
117
+ ### `tokio.connect(id, wsurl?)`
118
+
119
+ Connects to Tokio. Calling it again closes the previous connection first.
120
+
121
+ | Parameter | Description |
122
+ | --------- | ----------------------------------------------------------------------------------------- |
123
+ | `id` | The overlay ID, from `window.__OVERLAY_ID__`. Without one, it logs a warning and returns. |
124
+ | `wsurl` | The WebSocket server. Defaults to `ws://localhost:37891`. |
125
+
126
+ If Tokio isn't running or the connection drops, it keeps retrying: first after 2 to 3 seconds, then less often, up to every 30 seconds. Each attempt times out after 5 seconds.
127
+
128
+ Inside Tokio's own previews (the overlay cards in the app), it doesn't open a WebSocket. Tokio posts the events to the preview's iframe instead, and the loader only accepts them from the app page that embeds it.
129
+
130
+ ### `tokio.disconnect()`
131
+
132
+ Closes the connection. No `CLOSE` event is sent for it. Subscribers stay registered, so you can call `connect` again.
133
+
134
+ ### `tokio.on(event, callback)`
135
+
136
+ Subscribes `callback` to an event and returns a function that unsubscribes it. You can subscribe before or after `connect`. If a callback throws, the error is logged and the other callbacks still run.
137
+
138
+ ### `tokio.off(event, callback)`
139
+
140
+ Removes a callback added with `on`.
141
+
142
+ ## Events
143
+
144
+ Use the `TOKIO_EVENTS` constants. Messages with an unknown event are logged and ignored.
145
+
146
+ ### LIVE events
147
+
148
+ | Constant | Event name | Sent when |
149
+ | ---------------- | -------------------------- | --------------------------------------- |
150
+ | `CHAT` | `tokio:event:chat` | A viewer sends a chat message |
151
+ | `FOLLOW` | `tokio:event:follow` | A viewer follows you |
152
+ | `SHARE` | `tokio:event:share` | A viewer shares the LIVE |
153
+ | `LIKE` | `tokio:event:like` | A viewer likes the LIVE |
154
+ | `GIFT` | `tokio:event:gift` | A viewer sends a gift |
155
+ | `ENVELOPE` | `tokio:event:envelope` | A treasure envelope is sent |
156
+ | `SUPER_FAN` | `tokio:event:superfan` | A viewer becomes a Super Fan |
157
+ | `SUPER_FAN_BOX` | `tokio:event:superfanbox` | A Super Fan box is sent |
158
+ | `SUPER_FAN_JOIN` | `tokio:event:superfanjoin` | A Super Fan joins the LIVE |
159
+ | `MEMBER_JOIN` | `tokio:event:memberjoin` | A viewer joins the LIVE |
160
+ | `ROOM_USER` | `tokio:event:roomuser` | The viewer count and top viewers change |
161
+ | `MUSIC_CHANGE` | `tokio:event:music:change` | The music request queue changes |
162
+
163
+ ### Library events
164
+
165
+ | Constant | Event name | Sent when |
166
+ | -------- | -------------- | ------------------------------------------------------------------------------------- |
167
+ | `OPEN` | `tokio:open` | The connection to Tokio opens, also after each reconnect |
168
+ | `CLOSE` | `tokio:close` | The connection closes or an attempt fails. A failed one has code 1006, not `wasClean` |
169
+ | `ERROR` | `tokio:error` | The connection fails or times out |
170
+ | `CONFIG` | `tokio:config` | The streamer saves the overlay's settings |
171
+
172
+ ### Data
173
+
174
+ Each event's data has a type named after it:
175
+
176
+ | Event | Type | Data |
177
+ | ----------------------------------- | ------------------ | ----------------------------------------------------------------------- |
178
+ | `CHAT` | `ChatData` | `{ user, comment: { content, emotes, lang } }` |
179
+ | `FOLLOW` | `FollowData` | `{ user }` |
180
+ | `SHARE` | `ShareData` | `{ user, shareCount }` |
181
+ | `LIKE` | `LikeData` | `{ user, likeCount, totalLikes }` |
182
+ | `GIFT` | `GiftData` | `{ user, gift: { id, name, coins, type, comboCount, repeatEnd } }` |
183
+ | `SUPER_FAN` | `SuperFanData` | `{ user }` |
184
+ | `SUPER_FAN_JOIN` | `SuperFanJoinData` | `{ user }` |
185
+ | `MEMBER_JOIN` | `MemberJoinData` | `{ user }` |
186
+ | `ROOM_USER` | `RoomUserData` | `{ views, viewers, ranks: (UserData & { score, position })[] }` |
187
+ | `MUSIC_CHANGE` | `MusicChangeData` | The whole queue, `MusicInfo[]`: `{ title, thumbnail, playing, played }` |
188
+ | `CLOSE` | `CloseData` | `{ code, reason, wasClean }` |
189
+ | `ERROR` | `ErrorData` | `{ type: "error", message, error }` |
190
+ | `CONFIG` | `ConfigData` | The settings, the same object as `window.__OVERLAY_CONFIG__` |
191
+ | `ENVELOPE`, `SUPER_FAN_BOX`, `OPEN` | | none |
192
+
193
+ `user` is the viewer who triggered the event:
194
+
195
+ ```ts
196
+ interface UserData {
197
+ username: string; // display name
198
+ handle: string; // @handle
199
+ profilePictures: string[];
200
+ superfan?: boolean;
201
+ follower?: boolean;
202
+ moderator?: boolean;
203
+ fanLevel?: number;
204
+ }
205
+ ```
206
+
207
+ Each chat emote is `{ emoteID, index, images }`. A gift setting in `ConfigData` is a `CustomGift`: `{ value, label, img, coins }`.
208
+
209
+ A gift streak sends several `GIFT` events with a growing `comboCount`. The last one has `repeatEnd: true`; react to that one for a single reaction per combo.
210
+
211
+ ## TypeScript
212
+
213
+ Event data is typed from the event you subscribe to. To name a type, import it:
214
+
215
+ ```ts
216
+ import { tokio, TOKIO_EVENTS, type GiftData } from "@tokioapp/overlay";
217
+
218
+ const onGift = (data: GiftData) => {
219
+ if (data.gift.repeatEnd) showGift(data.user, data.gift);
220
+ };
221
+
222
+ tokio.on(TOKIO_EVENTS.GIFT, onGift);
223
+ ```
224
+
225
+ Besides the data types above, the package exports `UserData`, `Emotes`, `MusicInfo`, `CustomGift`, `EventMap` (each event's data) and `TokioPayload` (a message from Tokio).
226
+
227
+ To type the setup block's values:
228
+
229
+ ```ts
230
+ import type { ConfigData } from "@tokioapp/overlay";
231
+
232
+ declare global {
233
+ interface Window {
234
+ __OVERLAY_ID__: string;
235
+ __OVERLAY_CONFIG__: ConfigData; // generic config type, you can use your own.
236
+ }
237
+ }
238
+ ```
239
+
240
+ ## Raw WebSocket
241
+
242
+ Overlays you import into Tokio need the library, since Tokio's previews post their events to the overlay's iframe. A page you don't import can connect a WebSocket itself, to `ws://localhost:37891/?id=<overlay ID>`. Each message is JSON with the event name and its data:
243
+
244
+ ```json
245
+ {
246
+ "type": "tokio:event:like",
247
+ "data": { "user": {}, "likeCount": 5, "totalLikes": 1200 }
248
+ }
249
+ ```
250
+
251
+ ## Development
252
+
253
+ ```sh
254
+ yarn install
255
+ yarn build # dist/: index.es.js, index.cjs, index.umd.js and the types
256
+ yarn typecheck # tsc
257
+ yarn lint # oxlint
258
+ yarn format # prettier
259
+ ```
260
+
261
+ Commits follow [Conventional Commits](https://www.conventionalcommits.org/), checked by a commitlint hook.
262
+
263
+ ### Releasing
264
+
265
+ Bump `version` in `package.json`, commit, and push a matching tag:
266
+
267
+ ```sh
268
+ git tag v1.0.1
269
+ git push origin v1.0.1
270
+ ```
271
+
272
+ The [Publish](.github/workflows/publish.yml) workflow checks the code, publishes the version to npm and creates its GitHub release. Prereleases such as `1.1.0-beta.1` are published under the `next` tag.
273
+
274
+ ## License
275
+
276
+ [MIT](LICENSE)
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});var e="ws://localhost:37891",t=function(e){return e.OPEN="tokio:open",e.ERROR="tokio:error",e.CLOSE="tokio:close",e.CONFIG="tokio:config",e.CHAT="tokio:event:chat",e.SHARE="tokio:event:share",e.FOLLOW="tokio:event:follow",e.GIFT="tokio:event:gift",e.LIKE="tokio:event:like",e.ENVELOPE="tokio:event:envelope",e.SUPER_FAN="tokio:event:superfan",e.SUPER_FAN_BOX="tokio:event:superfanbox",e.SUPER_FAN_JOIN="tokio:event:superfanjoin",e.MEMBER_JOIN="tokio:event:memberjoin",e.ROOM_USER="tokio:event:roomuser",e.MUSIC_CHANGE="tokio:event:music:change",e}({}),s=class{target;type;constructor(e,t){this.target=t,this.type=e}},n=class extends s{message;error;constructor(e,t){super("error",t),e instanceof Error?(this.message=e.message,this.error=e):this.message=e}},o=class extends s{code;reason;wasClean;constructor(e=1e3,t="",s,n=!0){super("close",s),this.code=e,this.reason=t,this.wasClean=n}},i=()=>{if("undefined"!=typeof WebSocket)return WebSocket},r=function(e){return e[e.CONNECTING=0]="CONNECTING",e[e.OPEN=1]="OPEN",e[e.CLOSING=2]="CLOSING",e[e.CLOSED=3]="CLOSED",e}({}),c={maxReconnectionDelay:1e4,minReconnectionDelay:1e3+4e3*Math.random(),minUptime:5e3,reconnectionDelayGrowFactor:1.3,connectionTimeout:4e3,maxRetries:1/0,maxEnqueuedMessages:1/0,startClosed:!1,debug:!1},h=class{_ws;_listeners={error:[],message:[],open:[],close:[]};_retryCount=-1;_uptimeTimeout;_connectTimeout;_shouldReconnect=!0;_connectLock=!1;_binaryType="blob";_closeCalled=!1;_messageQueue=[];_url;_protocols;_options;constructor(e,t,s={}){this._url=e,this._protocols=t,this._options=s,this._options.startClosed&&(this._shouldReconnect=!1),this._connect()}get readyStateConstants(){return r}get binaryType(){return this._ws?this._ws.binaryType:this._binaryType}set binaryType(e){this._binaryType=e,this._ws&&(this._ws.binaryType=e)}get retryCount(){return Math.max(this._retryCount,0)}get bufferedAmount(){return this._messageQueue.reduce((e,t)=>"string"==typeof t?e+new Blob([t]).size:t instanceof Blob?e+t.size:e+t.byteLength,0)+(this._ws?this._ws.bufferedAmount:0)}get extensions(){return this._ws?this._ws.extensions:""}get protocol(){return this._ws?this._ws.protocol:""}get readyState(){return this._ws?this._ws.readyState:this._options.startClosed?3:0}get url(){return this._ws?this._ws.url:""}onclose=null;onerror=null;onmessage=null;onopen=null;close(e=1e3,t){this._closeCalled=!0,this._shouldReconnect=!1,this._clearTimeouts(),this._ws?3!==this._ws.readyState?this._ws.close(e,t):this._debug("close: already closed"):this._debug("close enqueued: no ws instance")}reconnect(e,t){this._shouldReconnect=!0,this._closeCalled=!1,this._retryCount=-1,this._ws&&3!==this._ws.readyState?(this._disconnect(e,t),this._connect()):this._connect()}send(e){if(this._ws&&1===this._ws.readyState)this._debug("send",e),this._ws.send(e);else{const{maxEnqueuedMessages:t=c.maxEnqueuedMessages}=this._options;this._messageQueue.length<t&&(this._debug("enqueue",e),this._messageQueue.push(e))}}addEventListener(e,t){const s=this._listeners[e];s&&s.push(t)}dispatchEvent(e){const t=this._listeners[e.type];if(t)for(const s of t)this._callEventListener(e,s);return!0}removeEventListener(e,t){if(this._listeners[e]){const s=this._listeners[e];this._listeners[e]=s.filter(e=>e!==t)}}_debug(...e){this._options.debug&&console.log.apply(console,["RWS>",...e])}_getNextDelay(){const{reconnectionDelayGrowFactor:e=c.reconnectionDelayGrowFactor,minReconnectionDelay:t=c.minReconnectionDelay,maxReconnectionDelay:s=c.maxReconnectionDelay}=this._options;let n=0;return this._retryCount>0&&(n=t*Math.pow(e,this._retryCount-1),n>s&&(n=s)),this._debug("next delay",n),n}_wait(){return new Promise(e=>{setTimeout(e,this._getNextDelay())})}async _getNextUrl(e){if("string"==typeof e)return e;if("function"==typeof e){const t=await Promise.resolve(e());if("string"==typeof t)return t}throw new Error("Invalid URL")}async _connect(){if(this._connectLock||!this._shouldReconnect)return;this._connectLock=!0;const{maxRetries:e=c.maxRetries,connectionTimeout:t=c.connectionTimeout,WebSocket:s=i()}=this._options;if(this._retryCount>=e)this._debug("max retries reached",this._retryCount,">=",e);else{if(this._retryCount++,this._debug("connect",this._retryCount),this._removeListeners(),null==s)throw new Error("No valid WebSocket class provided");try{await this._wait();const e=await this._getNextUrl(this._url);if(this._closeCalled)return void(this._connectLock=!1);this._debug("connect",{url:e,protocols:this._protocols}),this._ws=this._protocols?new s(e,this._protocols):new s(e),this._ws.binaryType=this._binaryType,this._connectLock=!1,this._addListeners(),this._connectTimeout=setTimeout(()=>this._handleTimeout(),t)}catch(o){this._connectLock=!1,this._handleError(new n(o instanceof Error?o:new Error(String(o)),this))}}}_handleTimeout(){this._debug("timeout event"),this._handleError(new n(new Error("timeout"),this))}_disconnect(e=1e3,t,s=!0){if(this._clearTimeouts(),this._ws){this._removeListeners();try{this._ws.close(e,t),this._handleClose(new o(s?e:1006,t,this,s))}catch{}}}_acceptOpen(){this._debug("accept open"),this._retryCount=0}_callEventListener(e,t){"function"==typeof t?t(e):t&&"handleEvent"in t&&t.handleEvent(e)}_handleOpen=e=>{this._debug("open event");const{minUptime:t=c.minUptime}=this._options;clearTimeout(this._connectTimeout),this._uptimeTimeout=setTimeout(()=>this._acceptOpen(),t),this._ws&&(this._ws.binaryType=this._binaryType);const n=e instanceof s?e:new s("open",this);this._messageQueue.forEach(e=>this._ws?.send(e)),this._messageQueue=[],this.onopen&&this.onopen(n),this._listeners.open.forEach(e=>this._callEventListener(n,e))};_handleMessage=e=>{this._debug("message event"),this.onmessage&&this.onmessage(e),this._listeners.message.forEach(t=>this._callEventListener(e,t))};_handleError=e=>{let t="WebSocket Error";e instanceof n&&(t=e.message),this._debug("error event",t),this._disconnect(void 0,"timeout"===t?"timeout":void 0,!1);const s=e instanceof n?e:new n(t,this);this.onerror&&this.onerror(s),this._debug("exec error listeners"),this._listeners.error.forEach(e=>this._callEventListener(s,e)),this._connect()};_handleClose=e=>{this._debug("close event"),this._clearTimeouts(),this._shouldReconnect&&this._connect();const t=e instanceof o?e:new o(e.code,e.reason,this);this.onclose&&this.onclose(t),this._listeners.close.forEach(e=>this._callEventListener(t,e))};_removeListeners(){this._ws&&(this._debug("removeListeners"),this._ws.removeEventListener("open",this._handleOpen),this._ws.removeEventListener("close",this._handleClose),this._ws.removeEventListener("message",this._handleMessage),this._ws.removeEventListener("error",this._handleError))}_addListeners(){this._ws&&(this._debug("addListeners"),this._ws.addEventListener("open",this._handleOpen),this._ws.addEventListener("close",this._handleClose),this._ws.addEventListener("message",this._handleMessage),this._ws.addEventListener("error",this._handleError))}_clearTimeouts(){clearTimeout(this._connectTimeout),clearTimeout(this._uptimeTimeout)}},a=new Set(Object.values(t).filter(e=>e!==t.OPEN&&e!==t.ERROR&&e!==t.CLOSE)),_=new class{_ws=null;_subscribers={};_iframeListener=null;connect(t,s){this.disconnect(),window.name===`TOKIO_APP-${t}`?this._initIframeListener():this._initWebSocket(t,s??e)}disconnect(){this._ws&&(this._ws.onopen=null,this._ws.onmessage=null,this._ws.onerror=null,this._ws.onclose=null,this._ws.close(),this._ws=null),this._iframeListener&&(window.removeEventListener("message",this._iframeListener),this._iframeListener=null)}on(e,t){return this._subscribers[e]||(this._subscribers[e]=new Set),this._subscribers[e].add(t),()=>this.off(e,t)}off(e,t){const s=this._subscribers[e];s&&s.delete(t)}_publish(e,t){const s=this._subscribers[e];s&&s.forEach(s=>{try{s(t)}catch(n){console.error(`Tokio subscriber for ${e} failed:`,n)}})}_initWebSocket(e,s){if(!e)return void console.warn("Tokio: connect() needs the overlay ID, not connecting.");const n=new URL(s);n.searchParams.set("id",e),this._ws=new h(n.toString(),[],{minReconnectionDelay:2e3+1e3*Math.random(),reconnectionDelayGrowFactor:1.5,maxReconnectionDelay:3e4,connectionTimeout:5e3}),this._ws.onopen=()=>{this._publish(t.OPEN,void 0)},this._ws.onmessage=e=>{let t;try{t=JSON.parse(e.data)}catch(s){return void console.error("Failed to parse WebSocket message:",s)}this._handleEvents(t)},this._ws.onerror=e=>{const s={type:"error",message:e.message||"WebSocket Error",error:e.error||new Error(e.message||"WebSocket Error")};this._publish(t.ERROR,s)},this._ws.onclose=e=>{const s={code:e.code,reason:e.reason,wasClean:e.wasClean};this._publish(t.CLOSE,s)}}_initIframeListener(){this._iframeListener=e=>{if(e.source!==window.parent)return;let t=e.data;if("string"==typeof t)try{t=JSON.parse(t)}catch{return}"object"==typeof t&&null!==t&&"action"in t&&"TOKIO_PREVIEW"===t.action&&"payload"in t&&this._handleEvents(t.payload)},window.addEventListener("message",this._iframeListener)}_handleEvents(e){if((e=>"object"==typeof e&&null!==e&&"type"in e&&"string"==typeof e.type&&a.has(e.type))(e))this._publish(e.type,e.data);else{const t="object"==typeof e&&null!==e&&"type"in e?e.type:void 0;console.warn(`Received unknown Tokio event type: ${String(t)}`)}}};exports.TOKIO_EVENTS=t,exports.tokio=_;
@@ -0,0 +1 @@
1
+ var e="ws://localhost:37891",t=/* @__PURE__ */function(e){return e.OPEN="tokio:open",e.ERROR="tokio:error",e.CLOSE="tokio:close",e.CONFIG="tokio:config",e.CHAT="tokio:event:chat",e.SHARE="tokio:event:share",e.FOLLOW="tokio:event:follow",e.GIFT="tokio:event:gift",e.LIKE="tokio:event:like",e.ENVELOPE="tokio:event:envelope",e.SUPER_FAN="tokio:event:superfan",e.SUPER_FAN_BOX="tokio:event:superfanbox",e.SUPER_FAN_JOIN="tokio:event:superfanjoin",e.MEMBER_JOIN="tokio:event:memberjoin",e.ROOM_USER="tokio:event:roomuser",e.MUSIC_CHANGE="tokio:event:music:change",e}({}),s=class{target;type;constructor(e,t){this.target=t,this.type=e}},n=class extends s{message;error;constructor(e,t){super("error",t),e instanceof Error?(this.message=e.message,this.error=e):this.message=e}},i=class extends s{code;reason;wasClean;constructor(e=1e3,t="",s,n=!0){super("close",s),this.code=e,this.reason=t,this.wasClean=n}},o=()=>{if("undefined"!=typeof WebSocket)return WebSocket},r=/* @__PURE__ */function(e){return e[e.CONNECTING=0]="CONNECTING",e[e.OPEN=1]="OPEN",e[e.CLOSING=2]="CLOSING",e[e.CLOSED=3]="CLOSED",e}({}),c={maxReconnectionDelay:1e4,minReconnectionDelay:1e3+4e3*Math.random(),minUptime:5e3,reconnectionDelayGrowFactor:1.3,connectionTimeout:4e3,maxRetries:1/0,maxEnqueuedMessages:1/0,startClosed:!1,debug:!1},h=class{_ws;_listeners={error:[],message:[],open:[],close:[]};_retryCount=-1;_uptimeTimeout;_connectTimeout;_shouldReconnect=!0;_connectLock=!1;_binaryType="blob";_closeCalled=!1;_messageQueue=[];_url;_protocols;_options;constructor(e,t,s={}){this._url=e,this._protocols=t,this._options=s,this._options.startClosed&&(this._shouldReconnect=!1),this._connect()}get readyStateConstants(){return r}get binaryType(){return this._ws?this._ws.binaryType:this._binaryType}set binaryType(e){this._binaryType=e,this._ws&&(this._ws.binaryType=e)}get retryCount(){return Math.max(this._retryCount,0)}get bufferedAmount(){return this._messageQueue.reduce((e,t)=>"string"==typeof t?e+new Blob([t]).size:t instanceof Blob?e+t.size:e+t.byteLength,0)+(this._ws?this._ws.bufferedAmount:0)}get extensions(){return this._ws?this._ws.extensions:""}get protocol(){return this._ws?this._ws.protocol:""}get readyState(){return this._ws?this._ws.readyState:this._options.startClosed?3:0}get url(){return this._ws?this._ws.url:""}onclose=null;onerror=null;onmessage=null;onopen=null;close(e=1e3,t){this._closeCalled=!0,this._shouldReconnect=!1,this._clearTimeouts(),this._ws?3!==this._ws.readyState?this._ws.close(e,t):this._debug("close: already closed"):this._debug("close enqueued: no ws instance")}reconnect(e,t){this._shouldReconnect=!0,this._closeCalled=!1,this._retryCount=-1,this._ws&&3!==this._ws.readyState?(this._disconnect(e,t),this._connect()):this._connect()}send(e){if(this._ws&&1===this._ws.readyState)this._debug("send",e),this._ws.send(e);else{const{maxEnqueuedMessages:t=c.maxEnqueuedMessages}=this._options;this._messageQueue.length<t&&(this._debug("enqueue",e),this._messageQueue.push(e))}}addEventListener(e,t){const s=this._listeners[e];s&&s.push(t)}dispatchEvent(e){const t=this._listeners[e.type];if(t)for(const s of t)this._callEventListener(e,s);return!0}removeEventListener(e,t){if(this._listeners[e]){const s=this._listeners[e];this._listeners[e]=s.filter(e=>e!==t)}}_debug(...e){this._options.debug&&console.log.apply(console,["RWS>",...e])}_getNextDelay(){const{reconnectionDelayGrowFactor:e=c.reconnectionDelayGrowFactor,minReconnectionDelay:t=c.minReconnectionDelay,maxReconnectionDelay:s=c.maxReconnectionDelay}=this._options;let n=0;return this._retryCount>0&&(n=t*Math.pow(e,this._retryCount-1),n>s&&(n=s)),this._debug("next delay",n),n}_wait(){return new Promise(e=>{setTimeout(e,this._getNextDelay())})}async _getNextUrl(e){if("string"==typeof e)return e;if("function"==typeof e){const t=await Promise.resolve(e());if("string"==typeof t)return t}throw new Error("Invalid URL")}async _connect(){if(this._connectLock||!this._shouldReconnect)return;this._connectLock=!0;const{maxRetries:e=c.maxRetries,connectionTimeout:t=c.connectionTimeout,WebSocket:s=o()}=this._options;if(this._retryCount>=e)this._debug("max retries reached",this._retryCount,">=",e);else{if(this._retryCount++,this._debug("connect",this._retryCount),this._removeListeners(),null==s)throw new Error("No valid WebSocket class provided");try{await this._wait();const e=await this._getNextUrl(this._url);if(this._closeCalled)return void(this._connectLock=!1);this._debug("connect",{url:e,protocols:this._protocols}),this._ws=this._protocols?new s(e,this._protocols):new s(e),this._ws.binaryType=this._binaryType,this._connectLock=!1,this._addListeners(),this._connectTimeout=setTimeout(()=>this._handleTimeout(),t)}catch(i){this._connectLock=!1,this._handleError(new n(i instanceof Error?i:new Error(String(i)),this))}}}_handleTimeout(){this._debug("timeout event"),this._handleError(new n(/* @__PURE__ */new Error("timeout"),this))}_disconnect(e=1e3,t,s=!0){if(this._clearTimeouts(),this._ws){this._removeListeners();try{this._ws.close(e,t),this._handleClose(new i(s?e:1006,t,this,s))}catch{}}}_acceptOpen(){this._debug("accept open"),this._retryCount=0}_callEventListener(e,t){"function"==typeof t?t(e):t&&"handleEvent"in t&&t.handleEvent(e)}_handleOpen=e=>{this._debug("open event");const{minUptime:t=c.minUptime}=this._options;clearTimeout(this._connectTimeout),this._uptimeTimeout=setTimeout(()=>this._acceptOpen(),t),this._ws&&(this._ws.binaryType=this._binaryType);const n=e instanceof s?e:new s("open",this);this._messageQueue.forEach(e=>this._ws?.send(e)),this._messageQueue=[],this.onopen&&this.onopen(n),this._listeners.open.forEach(e=>this._callEventListener(n,e))};_handleMessage=e=>{this._debug("message event"),this.onmessage&&this.onmessage(e),this._listeners.message.forEach(t=>this._callEventListener(e,t))};_handleError=e=>{let t="WebSocket Error";e instanceof n&&(t=e.message),this._debug("error event",t),this._disconnect(void 0,"timeout"===t?"timeout":void 0,!1);const s=e instanceof n?e:new n(t,this);this.onerror&&this.onerror(s),this._debug("exec error listeners"),this._listeners.error.forEach(e=>this._callEventListener(s,e)),this._connect()};_handleClose=e=>{this._debug("close event"),this._clearTimeouts(),this._shouldReconnect&&this._connect();const t=e instanceof i?e:new i(e.code,e.reason,this);this.onclose&&this.onclose(t),this._listeners.close.forEach(e=>this._callEventListener(t,e))};_removeListeners(){this._ws&&(this._debug("removeListeners"),this._ws.removeEventListener("open",this._handleOpen),this._ws.removeEventListener("close",this._handleClose),this._ws.removeEventListener("message",this._handleMessage),this._ws.removeEventListener("error",this._handleError))}_addListeners(){this._ws&&(this._debug("addListeners"),this._ws.addEventListener("open",this._handleOpen),this._ws.addEventListener("close",this._handleClose),this._ws.addEventListener("message",this._handleMessage),this._ws.addEventListener("error",this._handleError))}_clearTimeouts(){clearTimeout(this._connectTimeout),clearTimeout(this._uptimeTimeout)}},a=new Set(Object.values(t).filter(e=>e!==t.OPEN&&e!==t.ERROR&&e!==t.CLOSE)),_=new class{_ws=null;_subscribers={};_iframeListener=null;connect(t,s){this.disconnect(),window.name===`TOKIO_APP-${t}`?this._initIframeListener():this._initWebSocket(t,s??e)}disconnect(){this._ws&&(this._ws.onopen=null,this._ws.onmessage=null,this._ws.onerror=null,this._ws.onclose=null,this._ws.close(),this._ws=null),this._iframeListener&&(window.removeEventListener("message",this._iframeListener),this._iframeListener=null)}on(e,t){return this._subscribers[e]||(this._subscribers[e]=/* @__PURE__ */new Set),this._subscribers[e].add(t),()=>this.off(e,t)}off(e,t){const s=this._subscribers[e];s&&s.delete(t)}_publish(e,t){const s=this._subscribers[e];s&&s.forEach(s=>{try{s(t)}catch(n){console.error(`Tokio subscriber for ${e} failed:`,n)}})}_initWebSocket(e,s){if(!e)return void console.warn("Tokio: connect() needs the overlay ID, not connecting.");const n=new URL(s);n.searchParams.set("id",e),this._ws=new h(n.toString(),[],{minReconnectionDelay:2e3+1e3*Math.random(),reconnectionDelayGrowFactor:1.5,maxReconnectionDelay:3e4,connectionTimeout:5e3}),this._ws.onopen=()=>{this._publish(t.OPEN,void 0)},this._ws.onmessage=e=>{let t;try{t=JSON.parse(e.data)}catch(s){return void console.error("Failed to parse WebSocket message:",s)}this._handleEvents(t)},this._ws.onerror=e=>{const s={type:"error",message:e.message||"WebSocket Error",error:e.error||new Error(e.message||"WebSocket Error")};this._publish(t.ERROR,s)},this._ws.onclose=e=>{const s={code:e.code,reason:e.reason,wasClean:e.wasClean};this._publish(t.CLOSE,s)}}_initIframeListener(){this._iframeListener=e=>{if(e.source!==window.parent)return;let t=e.data;if("string"==typeof t)try{t=JSON.parse(t)}catch{return}"object"==typeof t&&null!==t&&"action"in t&&"TOKIO_PREVIEW"===t.action&&"payload"in t&&this._handleEvents(t.payload)},window.addEventListener("message",this._iframeListener)}_handleEvents(e){if((e=>"object"==typeof e&&null!==e&&"type"in e&&"string"==typeof e.type&&a.has(e.type))(e))this._publish(e.type,e.data);else{const t="object"==typeof e&&null!==e&&"type"in e?e.type:void 0;console.warn(`Received unknown Tokio event type: ${String(t)}`)}}};export{t as TOKIO_EVENTS,_ as tokio};
@@ -0,0 +1 @@
1
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).TokioOverlay={})}(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});var t="ws://localhost:37891",s=function(e){return e.OPEN="tokio:open",e.ERROR="tokio:error",e.CLOSE="tokio:close",e.CONFIG="tokio:config",e.CHAT="tokio:event:chat",e.SHARE="tokio:event:share",e.FOLLOW="tokio:event:follow",e.GIFT="tokio:event:gift",e.LIKE="tokio:event:like",e.ENVELOPE="tokio:event:envelope",e.SUPER_FAN="tokio:event:superfan",e.SUPER_FAN_BOX="tokio:event:superfanbox",e.SUPER_FAN_JOIN="tokio:event:superfanjoin",e.MEMBER_JOIN="tokio:event:memberjoin",e.ROOM_USER="tokio:event:roomuser",e.MUSIC_CHANGE="tokio:event:music:change",e}({}),n=class{target;type;constructor(e,t){this.target=t,this.type=e}},o=class extends n{message;error;constructor(e,t){super("error",t),e instanceof Error?(this.message=e.message,this.error=e):this.message=e}},i=class extends n{code;reason;wasClean;constructor(e=1e3,t="",s,n=!0){super("close",s),this.code=e,this.reason=t,this.wasClean=n}},r=()=>{if("undefined"!=typeof WebSocket)return WebSocket},c=function(e){return e[e.CONNECTING=0]="CONNECTING",e[e.OPEN=1]="OPEN",e[e.CLOSING=2]="CLOSING",e[e.CLOSED=3]="CLOSED",e}({}),h={maxReconnectionDelay:1e4,minReconnectionDelay:1e3+4e3*Math.random(),minUptime:5e3,reconnectionDelayGrowFactor:1.3,connectionTimeout:4e3,maxRetries:1/0,maxEnqueuedMessages:1/0,startClosed:!1,debug:!1},a=class{_ws;_listeners={error:[],message:[],open:[],close:[]};_retryCount=-1;_uptimeTimeout;_connectTimeout;_shouldReconnect=!0;_connectLock=!1;_binaryType="blob";_closeCalled=!1;_messageQueue=[];_url;_protocols;_options;constructor(e,t,s={}){this._url=e,this._protocols=t,this._options=s,this._options.startClosed&&(this._shouldReconnect=!1),this._connect()}get readyStateConstants(){return c}get binaryType(){return this._ws?this._ws.binaryType:this._binaryType}set binaryType(e){this._binaryType=e,this._ws&&(this._ws.binaryType=e)}get retryCount(){return Math.max(this._retryCount,0)}get bufferedAmount(){return this._messageQueue.reduce((e,t)=>"string"==typeof t?e+new Blob([t]).size:t instanceof Blob?e+t.size:e+t.byteLength,0)+(this._ws?this._ws.bufferedAmount:0)}get extensions(){return this._ws?this._ws.extensions:""}get protocol(){return this._ws?this._ws.protocol:""}get readyState(){return this._ws?this._ws.readyState:this._options.startClosed?3:0}get url(){return this._ws?this._ws.url:""}onclose=null;onerror=null;onmessage=null;onopen=null;close(e=1e3,t){this._closeCalled=!0,this._shouldReconnect=!1,this._clearTimeouts(),this._ws?3!==this._ws.readyState?this._ws.close(e,t):this._debug("close: already closed"):this._debug("close enqueued: no ws instance")}reconnect(e,t){this._shouldReconnect=!0,this._closeCalled=!1,this._retryCount=-1,this._ws&&3!==this._ws.readyState?(this._disconnect(e,t),this._connect()):this._connect()}send(e){if(this._ws&&1===this._ws.readyState)this._debug("send",e),this._ws.send(e);else{const{maxEnqueuedMessages:t=h.maxEnqueuedMessages}=this._options;this._messageQueue.length<t&&(this._debug("enqueue",e),this._messageQueue.push(e))}}addEventListener(e,t){const s=this._listeners[e];s&&s.push(t)}dispatchEvent(e){const t=this._listeners[e.type];if(t)for(const s of t)this._callEventListener(e,s);return!0}removeEventListener(e,t){if(this._listeners[e]){const s=this._listeners[e];this._listeners[e]=s.filter(e=>e!==t)}}_debug(...e){this._options.debug&&console.log.apply(console,["RWS>",...e])}_getNextDelay(){const{reconnectionDelayGrowFactor:e=h.reconnectionDelayGrowFactor,minReconnectionDelay:t=h.minReconnectionDelay,maxReconnectionDelay:s=h.maxReconnectionDelay}=this._options;let n=0;return this._retryCount>0&&(n=t*Math.pow(e,this._retryCount-1),n>s&&(n=s)),this._debug("next delay",n),n}_wait(){return new Promise(e=>{setTimeout(e,this._getNextDelay())})}async _getNextUrl(e){if("string"==typeof e)return e;if("function"==typeof e){const t=await Promise.resolve(e());if("string"==typeof t)return t}throw new Error("Invalid URL")}async _connect(){if(this._connectLock||!this._shouldReconnect)return;this._connectLock=!0;const{maxRetries:e=h.maxRetries,connectionTimeout:t=h.connectionTimeout,WebSocket:s=r()}=this._options;if(this._retryCount>=e)this._debug("max retries reached",this._retryCount,">=",e);else{if(this._retryCount++,this._debug("connect",this._retryCount),this._removeListeners(),null==s)throw new Error("No valid WebSocket class provided");try{await this._wait();const e=await this._getNextUrl(this._url);if(this._closeCalled)return void(this._connectLock=!1);this._debug("connect",{url:e,protocols:this._protocols}),this._ws=this._protocols?new s(e,this._protocols):new s(e),this._ws.binaryType=this._binaryType,this._connectLock=!1,this._addListeners(),this._connectTimeout=setTimeout(()=>this._handleTimeout(),t)}catch(n){this._connectLock=!1,this._handleError(new o(n instanceof Error?n:new Error(String(n)),this))}}}_handleTimeout(){this._debug("timeout event"),this._handleError(new o(new Error("timeout"),this))}_disconnect(e=1e3,t,s=!0){if(this._clearTimeouts(),this._ws){this._removeListeners();try{this._ws.close(e,t),this._handleClose(new i(s?e:1006,t,this,s))}catch{}}}_acceptOpen(){this._debug("accept open"),this._retryCount=0}_callEventListener(e,t){"function"==typeof t?t(e):t&&"handleEvent"in t&&t.handleEvent(e)}_handleOpen=e=>{this._debug("open event");const{minUptime:t=h.minUptime}=this._options;clearTimeout(this._connectTimeout),this._uptimeTimeout=setTimeout(()=>this._acceptOpen(),t),this._ws&&(this._ws.binaryType=this._binaryType);const s=e instanceof n?e:new n("open",this);this._messageQueue.forEach(e=>this._ws?.send(e)),this._messageQueue=[],this.onopen&&this.onopen(s),this._listeners.open.forEach(e=>this._callEventListener(s,e))};_handleMessage=e=>{this._debug("message event"),this.onmessage&&this.onmessage(e),this._listeners.message.forEach(t=>this._callEventListener(e,t))};_handleError=e=>{let t="WebSocket Error";e instanceof o&&(t=e.message),this._debug("error event",t),this._disconnect(void 0,"timeout"===t?"timeout":void 0,!1);const s=e instanceof o?e:new o(t,this);this.onerror&&this.onerror(s),this._debug("exec error listeners"),this._listeners.error.forEach(e=>this._callEventListener(s,e)),this._connect()};_handleClose=e=>{this._debug("close event"),this._clearTimeouts(),this._shouldReconnect&&this._connect();const t=e instanceof i?e:new i(e.code,e.reason,this);this.onclose&&this.onclose(t),this._listeners.close.forEach(e=>this._callEventListener(t,e))};_removeListeners(){this._ws&&(this._debug("removeListeners"),this._ws.removeEventListener("open",this._handleOpen),this._ws.removeEventListener("close",this._handleClose),this._ws.removeEventListener("message",this._handleMessage),this._ws.removeEventListener("error",this._handleError))}_addListeners(){this._ws&&(this._debug("addListeners"),this._ws.addEventListener("open",this._handleOpen),this._ws.addEventListener("close",this._handleClose),this._ws.addEventListener("message",this._handleMessage),this._ws.addEventListener("error",this._handleError))}_clearTimeouts(){clearTimeout(this._connectTimeout),clearTimeout(this._uptimeTimeout)}},_=new Set(Object.values(s).filter(e=>e!==s.OPEN&&e!==s.ERROR&&e!==s.CLOSE)),l=new class{_ws=null;_subscribers={};_iframeListener=null;connect(e,s){this.disconnect(),window.name===`TOKIO_APP-${e}`?this._initIframeListener():this._initWebSocket(e,s??t)}disconnect(){this._ws&&(this._ws.onopen=null,this._ws.onmessage=null,this._ws.onerror=null,this._ws.onclose=null,this._ws.close(),this._ws=null),this._iframeListener&&(window.removeEventListener("message",this._iframeListener),this._iframeListener=null)}on(e,t){return this._subscribers[e]||(this._subscribers[e]=new Set),this._subscribers[e].add(t),()=>this.off(e,t)}off(e,t){const s=this._subscribers[e];s&&s.delete(t)}_publish(e,t){const s=this._subscribers[e];s&&s.forEach(s=>{try{s(t)}catch(n){console.error(`Tokio subscriber for ${e} failed:`,n)}})}_initWebSocket(e,t){if(!e)return void console.warn("Tokio: connect() needs the overlay ID, not connecting.");const n=new URL(t);n.searchParams.set("id",e),this._ws=new a(n.toString(),[],{minReconnectionDelay:2e3+1e3*Math.random(),reconnectionDelayGrowFactor:1.5,maxReconnectionDelay:3e4,connectionTimeout:5e3}),this._ws.onopen=()=>{this._publish(s.OPEN,void 0)},this._ws.onmessage=e=>{let t;try{t=JSON.parse(e.data)}catch(s){return void console.error("Failed to parse WebSocket message:",s)}this._handleEvents(t)},this._ws.onerror=e=>{const t={type:"error",message:e.message||"WebSocket Error",error:e.error||new Error(e.message||"WebSocket Error")};this._publish(s.ERROR,t)},this._ws.onclose=e=>{const t={code:e.code,reason:e.reason,wasClean:e.wasClean};this._publish(s.CLOSE,t)}}_initIframeListener(){this._iframeListener=e=>{if(e.source!==window.parent)return;let t=e.data;if("string"==typeof t)try{t=JSON.parse(t)}catch{return}"object"==typeof t&&null!==t&&"action"in t&&"TOKIO_PREVIEW"===t.action&&"payload"in t&&this._handleEvents(t.payload)},window.addEventListener("message",this._iframeListener)}_handleEvents(e){if((e=>"object"==typeof e&&null!==e&&"type"in e&&"string"==typeof e.type&&_.has(e.type))(e))this._publish(e.type,e.data);else{const t="object"==typeof e&&null!==e&&"type"in e?e.type:void 0;console.warn(`Received unknown Tokio event type: ${String(t)}`)}}};e.TOKIO_EVENTS=s,e.tokio=l});
@@ -0,0 +1,4 @@
1
+ declare const _default: {
2
+ WS_URL: string;
3
+ };
4
+ export default _default;
@@ -0,0 +1,47 @@
1
+ import { EventMap, TOKIO_EVENTS } from '../types';
2
+ export { TOKIO_EVENTS };
3
+ export declare class EventLoader {
4
+ private _ws;
5
+ private _subscribers;
6
+ private _iframeListener;
7
+ /**
8
+ * Initializes the event loader. Inside a `Tokio App` preview it listens for the
9
+ * events the App posts to the iframe, anywhere else it connects to the WebSocket server.
10
+ *
11
+ * @param id - Overlay ID assigned by the `Tokio App`.
12
+ * @param wsurl - Optional WebSocket server URL. If omitted, it falls back to `Tokio App default`.
13
+ */
14
+ connect(id: string, wsurl?: string): void;
15
+ /**
16
+ * Closes the active WebSocket connection and stops listening for previews.
17
+ * Subscribers stay registered, and no CLOSE event is sent for this connection.
18
+ */
19
+ disconnect(): void;
20
+ /**
21
+ * Subscribes a callback function to a specific `Tokio` event.
22
+ *
23
+ * @param event - The name of the event to listen for.
24
+ * @param callback - The callback function executed when the event fires.
25
+ * @returns A cleanup function that unsubscribes this specific listener when called.
26
+ *
27
+ * @example
28
+ * const unsubscribe = tokio.on(TOKIO_EVENTS.CHAT, (data) => {
29
+ * console.log('Received CHAT payload:', data);
30
+ * });
31
+ *
32
+ * // Later, to unsubscribe:
33
+ * unsubscribe();
34
+ */
35
+ on<K extends TOKIO_EVENTS>(event: K, callback: (payload: EventMap[K]) => void): () => void;
36
+ /**
37
+ * Removes a previously registered callback function from an event.
38
+ *
39
+ * @param event - The event name the callback was subscribed to.
40
+ * @param callback - The exact callback function instance to remove.
41
+ */
42
+ off<K extends TOKIO_EVENTS>(event: K, callback: (payload: EventMap[K]) => void): void;
43
+ private _publish;
44
+ private _initWebSocket;
45
+ private _initIframeListener;
46
+ private _handleEvents;
47
+ }
@@ -0,0 +1,36 @@
1
+ export declare class Event {
2
+ target: unknown;
3
+ type: string;
4
+ constructor(type: string, target: unknown);
5
+ }
6
+ export declare class ErrorEvent extends Event {
7
+ message: string;
8
+ error?: Error;
9
+ constructor(error: Error | string, target: unknown);
10
+ }
11
+ export declare class CloseEvent extends Event {
12
+ code: number;
13
+ reason: string;
14
+ wasClean: boolean;
15
+ constructor(code: number | undefined, reason: string | undefined, target: unknown, wasClean?: boolean);
16
+ }
17
+ export interface WebSocketEventMap {
18
+ close: CloseEvent;
19
+ error: ErrorEvent;
20
+ message: MessageEvent;
21
+ open: Event;
22
+ }
23
+ export interface WebSocketEventListenerMap {
24
+ close: ((event: CloseEvent) => void) | {
25
+ handleEvent: (event: CloseEvent) => void;
26
+ };
27
+ error: ((event: ErrorEvent) => void) | {
28
+ handleEvent: (event: ErrorEvent) => void;
29
+ };
30
+ message: ((event: MessageEvent) => void) | {
31
+ handleEvent: (event: MessageEvent) => void;
32
+ };
33
+ open: ((event: Event) => void) | {
34
+ handleEvent: (event: Event) => void;
35
+ };
36
+ }
@@ -0,0 +1,85 @@
1
+ import * as Events from "./events";
2
+ interface WebSocketConstructor {
3
+ new (url: string, protocols?: string | string[]): WebSocket;
4
+ }
5
+ export declare enum ReadyState {
6
+ CONNECTING = 0,
7
+ OPEN = 1,
8
+ CLOSING = 2,
9
+ CLOSED = 3
10
+ }
11
+ export type Event = Events.Event;
12
+ export type ErrorEvent = Events.ErrorEvent;
13
+ export type CloseEvent = Events.CloseEvent;
14
+ export type Options = {
15
+ WebSocket?: WebSocketConstructor;
16
+ maxReconnectionDelay?: number;
17
+ minReconnectionDelay?: number;
18
+ reconnectionDelayGrowFactor?: number;
19
+ minUptime?: number;
20
+ connectionTimeout?: number;
21
+ maxRetries?: number;
22
+ maxEnqueuedMessages?: number;
23
+ startClosed?: boolean;
24
+ debug?: boolean;
25
+ };
26
+ export type UrlProvider = string | (() => string) | (() => Promise<string>);
27
+ export type Message = string | ArrayBuffer | Blob | ArrayBufferView;
28
+ export type ListenersMap = {
29
+ error: Array<Events.WebSocketEventListenerMap["error"]>;
30
+ message: Array<Events.WebSocketEventListenerMap["message"]>;
31
+ open: Array<Events.WebSocketEventListenerMap["open"]>;
32
+ close: Array<Events.WebSocketEventListenerMap["close"]>;
33
+ };
34
+ export default class ReconnectingWebSocket {
35
+ private _ws?;
36
+ private _listeners;
37
+ private _retryCount;
38
+ private _uptimeTimeout?;
39
+ private _connectTimeout?;
40
+ private _shouldReconnect;
41
+ private _connectLock;
42
+ private _binaryType;
43
+ private _closeCalled;
44
+ private _messageQueue;
45
+ private readonly _url;
46
+ private readonly _protocols?;
47
+ private readonly _options;
48
+ constructor(url: UrlProvider, protocols?: string | string[], options?: Options);
49
+ get readyStateConstants(): typeof ReadyState;
50
+ get binaryType(): BinaryType;
51
+ set binaryType(value: BinaryType);
52
+ get retryCount(): number;
53
+ get bufferedAmount(): number;
54
+ get extensions(): string;
55
+ get protocol(): string;
56
+ get readyState(): ReadyState;
57
+ get url(): string;
58
+ onclose: ((event: Events.CloseEvent) => void) | null;
59
+ onerror: ((event: Events.ErrorEvent) => void) | null;
60
+ onmessage: ((event: MessageEvent) => void) | null;
61
+ onopen: ((event: Event) => void) | null;
62
+ close(code?: number, reason?: string): void;
63
+ reconnect(code?: number, reason?: string): void;
64
+ send(data: Message): void;
65
+ addEventListener<T extends keyof Events.WebSocketEventListenerMap>(type: T, listener: Events.WebSocketEventListenerMap[T]): void;
66
+ dispatchEvent(event: Event): boolean;
67
+ removeEventListener<T extends keyof Events.WebSocketEventListenerMap>(type: T, listener: Events.WebSocketEventListenerMap[T]): void;
68
+ private _debug;
69
+ private _getNextDelay;
70
+ private _wait;
71
+ private _getNextUrl;
72
+ private _connect;
73
+ private _handleTimeout;
74
+ private _disconnect;
75
+ private _acceptOpen;
76
+ private _callEventListener;
77
+ private _handleOpen;
78
+ private _handleMessage;
79
+ private _handleError;
80
+ private _handleClose;
81
+ private _removeListeners;
82
+ private _addListeners;
83
+ private _clearTimeouts;
84
+ }
85
+ export {};
@@ -0,0 +1,5 @@
1
+ import { EventLoader, TOKIO_EVENTS } from './core';
2
+ declare const tokio: EventLoader;
3
+ export { tokio, TOKIO_EVENTS };
4
+ export type { CloseData, ConfigData, CustomGift, ErrorData, EventMap, TokioPayload, } from './types';
5
+ export type { ChatData, Emotes, FollowData, GiftData, LikeData, MemberJoinData, MusicChangeData, MusicInfo, RoomUserData, ShareData, SuperFanData, SuperFanJoinData, UserData, } from './types/events';
@@ -0,0 +1,65 @@
1
+ export interface UserData {
2
+ username: string;
3
+ handle: string;
4
+ profilePictures: string[];
5
+ superfan?: boolean;
6
+ follower?: boolean;
7
+ moderator?: boolean;
8
+ fanLevel?: number;
9
+ }
10
+ export interface ChatData {
11
+ user: UserData;
12
+ comment: {
13
+ content: string;
14
+ emotes: Emotes[];
15
+ lang: string;
16
+ };
17
+ }
18
+ export type RoomUserData = {
19
+ ranks: (UserData & {
20
+ score: number;
21
+ position: number;
22
+ })[];
23
+ views: number;
24
+ viewers: number;
25
+ };
26
+ export interface Emotes {
27
+ emoteID: string;
28
+ index: number;
29
+ images: string[];
30
+ }
31
+ export interface ShareData {
32
+ shareCount: number;
33
+ user: UserData;
34
+ }
35
+ interface SingleUserData {
36
+ user: UserData;
37
+ }
38
+ export type FollowData = SingleUserData;
39
+ export type SuperFanData = SingleUserData;
40
+ export type MemberJoinData = SingleUserData;
41
+ export type SuperFanJoinData = SingleUserData;
42
+ export interface GiftData {
43
+ user: UserData;
44
+ gift: {
45
+ id: string;
46
+ coins: number;
47
+ repeatEnd: boolean;
48
+ type: number;
49
+ name: string;
50
+ comboCount: number;
51
+ };
52
+ }
53
+ export interface LikeData {
54
+ user: UserData;
55
+ likeCount: number;
56
+ totalLikes: number;
57
+ }
58
+ export type MusicInfo = {
59
+ title: string;
60
+ thumbnail: string;
61
+ playing: boolean;
62
+ played: boolean;
63
+ };
64
+ export type MusicChangeData = MusicInfo[];
65
+ export {};
@@ -0,0 +1,72 @@
1
+ import { ChatData, FollowData, GiftData, LikeData, MemberJoinData, MusicChangeData, RoomUserData, ShareData, SuperFanData, SuperFanJoinData } from './events';
2
+ export type CustomGift = {
3
+ value: number;
4
+ label: string;
5
+ img: string;
6
+ coins: number;
7
+ };
8
+ type ConfigValue = string | number | boolean | string[] | number[] | CustomGift | CustomGift[] | undefined;
9
+ export type ConfigData = {
10
+ [id: string]: ConfigValue;
11
+ };
12
+ export declare enum TOKIO_EVENTS {
13
+ OPEN = "tokio:open",
14
+ ERROR = "tokio:error",
15
+ CLOSE = "tokio:close",
16
+ CONFIG = "tokio:config",
17
+ CHAT = "tokio:event:chat",
18
+ SHARE = "tokio:event:share",
19
+ FOLLOW = "tokio:event:follow",
20
+ GIFT = "tokio:event:gift",
21
+ LIKE = "tokio:event:like",
22
+ ENVELOPE = "tokio:event:envelope",
23
+ SUPER_FAN = "tokio:event:superfan",
24
+ SUPER_FAN_BOX = "tokio:event:superfanbox",
25
+ SUPER_FAN_JOIN = "tokio:event:superfanjoin",
26
+ MEMBER_JOIN = "tokio:event:memberjoin",
27
+ ROOM_USER = "tokio:event:roomuser",
28
+ MUSIC_CHANGE = "tokio:event:music:change"
29
+ }
30
+ export interface EventMap {
31
+ [TOKIO_EVENTS.OPEN]: void;
32
+ [TOKIO_EVENTS.ERROR]: ErrorData;
33
+ [TOKIO_EVENTS.CLOSE]: CloseData;
34
+ [TOKIO_EVENTS.CONFIG]: ConfigData;
35
+ [TOKIO_EVENTS.CHAT]: ChatData;
36
+ [TOKIO_EVENTS.SHARE]: ShareData;
37
+ [TOKIO_EVENTS.FOLLOW]: FollowData;
38
+ [TOKIO_EVENTS.GIFT]: GiftData;
39
+ [TOKIO_EVENTS.LIKE]: LikeData;
40
+ [TOKIO_EVENTS.ENVELOPE]: void;
41
+ [TOKIO_EVENTS.SUPER_FAN_BOX]: void;
42
+ [TOKIO_EVENTS.SUPER_FAN]: SuperFanData;
43
+ [TOKIO_EVENTS.MEMBER_JOIN]: MemberJoinData;
44
+ [TOKIO_EVENTS.SUPER_FAN_JOIN]: SuperFanJoinData;
45
+ [TOKIO_EVENTS.ROOM_USER]: RoomUserData;
46
+ [TOKIO_EVENTS.MUSIC_CHANGE]: MusicChangeData;
47
+ }
48
+ export type AppEvents = Exclude<TOKIO_EVENTS, TOKIO_EVENTS.OPEN | TOKIO_EVENTS.ERROR | TOKIO_EVENTS.CLOSE>;
49
+ export type TokioPayload = {
50
+ [K in AppEvents]: {
51
+ type: K;
52
+ data: EventMap[K];
53
+ };
54
+ }[AppEvents];
55
+ export interface TokioPreviewMessage {
56
+ action: "TOKIO_PREVIEW";
57
+ payload: TokioPayload;
58
+ }
59
+ export interface ErrorData {
60
+ message?: string;
61
+ type: string;
62
+ error?: Error;
63
+ }
64
+ export interface CloseData {
65
+ code: number;
66
+ reason: string;
67
+ wasClean: boolean;
68
+ }
69
+ export type Subscribers = {
70
+ [K in TOKIO_EVENTS]?: Set<(payload: EventMap[K]) => void>;
71
+ };
72
+ export {};
package/package.json CHANGED
@@ -1,6 +1,64 @@
1
1
  {
2
2
  "name": "@tokioapp/overlay",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Receive TikTok LIVE events from Tokio in your stream overlays.",
5
+ "type": "module",
6
+ "main": "dist/index.cjs",
7
+ "module": "dist/index.es.js",
8
+ "types": "./dist/types/index.d.ts",
9
+ "private": false,
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/types/index.d.ts",
13
+ "import": "./dist/index.es.js",
14
+ "require": "./dist/index.cjs"
15
+ }
16
+ },
17
+ "unpkg": "dist/index.umd.js",
18
+ "jsdelivr": "dist/index.umd.js",
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "scripts": {
26
+ "format": "prettier --write .",
27
+ "lint": "oxlint --type-aware",
28
+ "typecheck": "tsc --noEmit -p .",
29
+ "build": "vite build",
30
+ "prepublishOnly": "yarn build",
31
+ "prepare": "husky"
32
+ },
33
+ "author": "Akuqt",
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/tokio-app/overlay.git"
38
+ },
39
+ "keywords": [
40
+ "tiktok",
41
+ "overlays",
42
+ "alerts",
43
+ "tokio"
44
+ ],
45
+ "bugs": {
46
+ "url": "https://github.com/tokio-app/overlay/issues"
47
+ },
48
+ "homepage": "https://tokio.ialfaro.com",
49
+ "devDependencies": {
50
+ "@commitlint/cli": "^21.2.2",
51
+ "@commitlint/config-conventional": "^21.2.2",
52
+ "@typescript/typescript6": "^6.0.2",
53
+ "fallow": "^3.22.0",
54
+ "husky": "^9.1.7",
55
+ "lint-staged": "^17.4.1",
56
+ "oxlint": "^1.81.0",
57
+ "oxlint-tsgolint": "^7.0.2001",
58
+ "prettier": "^3.9.6",
59
+ "terser": "^5.51.2",
60
+ "typescript": "^7.0.2",
61
+ "vite": "^8.2.2",
62
+ "vite-plugin-dts": "^5.1.0"
63
+ }
64
+ }