@mygo-plugins/websocket 0.1.8
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 +21 -0
- package/README.md +33 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +225 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MyGo contributors
|
|
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
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @mygo-plugins/websocket
|
|
2
|
+
|
|
3
|
+
A `WebSocket` for [MyGo](https://github.com/egoist/mygo) apps whose
|
|
4
|
+
connections Go makes, with the API of the browser's. Unlike the browser's,
|
|
5
|
+
it may send any header with the opening handshake, such as Authorization,
|
|
6
|
+
Cookie or Origin, and is not subject to the page's Content Security Policy.
|
|
7
|
+
|
|
8
|
+
Use the plugin in Go:
|
|
9
|
+
|
|
10
|
+
```go
|
|
11
|
+
import "github.com/egoist/mygo/plugins/websocket"
|
|
12
|
+
|
|
13
|
+
mygo.Use(websocket.Plugin) // or websocket.New(websocket.Options{...})
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
and connect in the frontend:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { WebSocket } from "@mygo-plugins/websocket";
|
|
20
|
+
|
|
21
|
+
const ws = new WebSocket("wss://example.com/live", ["v1"], {
|
|
22
|
+
headers: { authorization: `Bearer ${token}` },
|
|
23
|
+
});
|
|
24
|
+
ws.onopen = () => ws.send("hello");
|
|
25
|
+
ws.onmessage = (e) => console.log(e.data);
|
|
26
|
+
ws.onclose = (e) => console.log(e.code, e.reason, e.wasClean);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Messages are sent in order, `bufferedAmount` counts the bytes Go has not
|
|
30
|
+
written yet, and received messages wait in Go while the page is behind, so a
|
|
31
|
+
fast server cannot fill the page's memory. Connections close (1001, going
|
|
32
|
+
away) when their page navigates away or its window closes. No extensions
|
|
33
|
+
(such as permessage-deflate) are negotiated.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Options of a {@link WebSocket}, beyond the browser's. */
|
|
2
|
+
export interface WebSocketOptions {
|
|
3
|
+
/** Headers of the opening handshake, e.g. Authorization or Origin. */
|
|
4
|
+
headers?: HeadersInit;
|
|
5
|
+
}
|
|
6
|
+
export interface WebSocketEventMap {
|
|
7
|
+
open: Event;
|
|
8
|
+
message: MessageEvent;
|
|
9
|
+
error: Event;
|
|
10
|
+
close: CloseEvent;
|
|
11
|
+
}
|
|
12
|
+
type Handler<E> = ((this: WebSocket, ev: E) => unknown) | null;
|
|
13
|
+
/**
|
|
14
|
+
* A WebSocket connection made by Go, with the API of the browser's
|
|
15
|
+
* `WebSocket`. The third argument sets headers of the opening handshake.
|
|
16
|
+
*/
|
|
17
|
+
export declare class WebSocket extends EventTarget {
|
|
18
|
+
#private;
|
|
19
|
+
static readonly CONNECTING = 0;
|
|
20
|
+
static readonly OPEN = 1;
|
|
21
|
+
static readonly CLOSING = 2;
|
|
22
|
+
static readonly CLOSED = 3;
|
|
23
|
+
readonly CONNECTING: 0;
|
|
24
|
+
readonly OPEN: 1;
|
|
25
|
+
readonly CLOSING: 2;
|
|
26
|
+
readonly CLOSED: 3;
|
|
27
|
+
/** The URL of the connection, with a ws: or wss: scheme. */
|
|
28
|
+
readonly url: string;
|
|
29
|
+
/** How binary messages arrive: as a `Blob`, or an `ArrayBuffer`. */
|
|
30
|
+
binaryType: BinaryType;
|
|
31
|
+
constructor(url: string | URL, protocols?: string | string[], options?: WebSocketOptions);
|
|
32
|
+
/** The state of the connection: CONNECTING, OPEN, CLOSING or CLOSED. */
|
|
33
|
+
get readyState(): number;
|
|
34
|
+
/** The subprotocol the server chose, once open. */
|
|
35
|
+
get protocol(): string;
|
|
36
|
+
/** The extensions in use: none. */
|
|
37
|
+
get extensions(): string;
|
|
38
|
+
/** Bytes sent with `send` that Go has not written yet. */
|
|
39
|
+
get bufferedAmount(): number;
|
|
40
|
+
get onopen(): Handler<Event>;
|
|
41
|
+
set onopen(fn: Handler<Event>);
|
|
42
|
+
get onmessage(): Handler<MessageEvent>;
|
|
43
|
+
set onmessage(fn: Handler<MessageEvent>);
|
|
44
|
+
get onerror(): Handler<Event>;
|
|
45
|
+
set onerror(fn: Handler<Event>);
|
|
46
|
+
get onclose(): Handler<CloseEvent>;
|
|
47
|
+
set onclose(fn: Handler<CloseEvent>);
|
|
48
|
+
/** Sends a message: text, or binary data. Throws while connecting. */
|
|
49
|
+
send(data: string | ArrayBufferLike | ArrayBufferView | Blob): void;
|
|
50
|
+
/**
|
|
51
|
+
* Closes the connection with a code (1000, or 3000 to 4999) and a reason
|
|
52
|
+
* of at most 123 bytes.
|
|
53
|
+
*/
|
|
54
|
+
close(code?: number, reason?: string): void;
|
|
55
|
+
}
|
|
56
|
+
export interface WebSocket {
|
|
57
|
+
addEventListener<K extends keyof WebSocketEventMap>(type: K, listener: (this: WebSocket, ev: WebSocketEventMap[K]) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
58
|
+
addEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void;
|
|
59
|
+
removeEventListener<K extends keyof WebSocketEventMap>(type: K, listener: (this: WebSocket, ev: WebSocketEventMap[K]) => unknown, options?: boolean | EventListenerOptions): void;
|
|
60
|
+
removeEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions): void;
|
|
61
|
+
}
|
|
62
|
+
export default WebSocket;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { Channel, call, isCallError } from "mygo-runtime";
|
|
3
|
+
var CONNECTING = 0;
|
|
4
|
+
var OPEN = 1;
|
|
5
|
+
var CLOSING = 2;
|
|
6
|
+
var CLOSED = 3;
|
|
7
|
+
var encoder = new TextEncoder;
|
|
8
|
+
|
|
9
|
+
class WebSocket extends EventTarget {
|
|
10
|
+
static CONNECTING = CONNECTING;
|
|
11
|
+
static OPEN = OPEN;
|
|
12
|
+
static CLOSING = CLOSING;
|
|
13
|
+
static CLOSED = CLOSED;
|
|
14
|
+
url;
|
|
15
|
+
binaryType = "blob";
|
|
16
|
+
#id;
|
|
17
|
+
#state = CONNECTING;
|
|
18
|
+
#protocol = "";
|
|
19
|
+
#buffered = 0;
|
|
20
|
+
#seq = 0;
|
|
21
|
+
#failed;
|
|
22
|
+
#events;
|
|
23
|
+
#handlers = new Map;
|
|
24
|
+
constructor(url, protocols = [], options = {}) {
|
|
25
|
+
super();
|
|
26
|
+
const base = typeof location !== "undefined" ? location.href : undefined;
|
|
27
|
+
let u;
|
|
28
|
+
try {
|
|
29
|
+
u = new URL(url, base);
|
|
30
|
+
} catch {
|
|
31
|
+
throw new DOMException(`Invalid URL: ${url}`, "SyntaxError");
|
|
32
|
+
}
|
|
33
|
+
if (u.protocol === "http:")
|
|
34
|
+
u.protocol = "ws:";
|
|
35
|
+
else if (u.protocol === "https:")
|
|
36
|
+
u.protocol = "wss:";
|
|
37
|
+
if (u.protocol !== "ws:" && u.protocol !== "wss:") {
|
|
38
|
+
throw new DOMException(`The URL's scheme must be ws or wss, not ${u.protocol.slice(0, -1)}`, "SyntaxError");
|
|
39
|
+
}
|
|
40
|
+
if (u.hash)
|
|
41
|
+
throw new DOMException("The URL contains a fragment", "SyntaxError");
|
|
42
|
+
const list = typeof protocols === "string" ? [protocols] : [...protocols];
|
|
43
|
+
if (new Set(list).size !== list.length) {
|
|
44
|
+
throw new DOMException("The subprotocols contain duplicates", "SyntaxError");
|
|
45
|
+
}
|
|
46
|
+
this.url = u.href;
|
|
47
|
+
this.#id = globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
48
|
+
this.#events = new Channel((e) => this.#receive(e));
|
|
49
|
+
call("plugin:websocket.Connect", {
|
|
50
|
+
id: this.#id,
|
|
51
|
+
url: this.url,
|
|
52
|
+
protocols: list,
|
|
53
|
+
headers: [...new Headers(options.headers)]
|
|
54
|
+
}, this.#events).then(() => this.#finish(1006, "", false, this.#failed ?? "connection lost"), (err) => this.#finish(1006, "", false, this.#failed ?? failure(err)));
|
|
55
|
+
}
|
|
56
|
+
get readyState() {
|
|
57
|
+
return this.#state;
|
|
58
|
+
}
|
|
59
|
+
get protocol() {
|
|
60
|
+
return this.#protocol;
|
|
61
|
+
}
|
|
62
|
+
get extensions() {
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
65
|
+
get bufferedAmount() {
|
|
66
|
+
return this.#buffered;
|
|
67
|
+
}
|
|
68
|
+
get onopen() {
|
|
69
|
+
return this.#handler("open");
|
|
70
|
+
}
|
|
71
|
+
set onopen(fn) {
|
|
72
|
+
this.#setHandler("open", fn);
|
|
73
|
+
}
|
|
74
|
+
get onmessage() {
|
|
75
|
+
return this.#handler("message");
|
|
76
|
+
}
|
|
77
|
+
set onmessage(fn) {
|
|
78
|
+
this.#setHandler("message", fn);
|
|
79
|
+
}
|
|
80
|
+
get onerror() {
|
|
81
|
+
return this.#handler("error");
|
|
82
|
+
}
|
|
83
|
+
set onerror(fn) {
|
|
84
|
+
this.#setHandler("error", fn);
|
|
85
|
+
}
|
|
86
|
+
get onclose() {
|
|
87
|
+
return this.#handler("close");
|
|
88
|
+
}
|
|
89
|
+
set onclose(fn) {
|
|
90
|
+
this.#setHandler("close", fn);
|
|
91
|
+
}
|
|
92
|
+
send(data) {
|
|
93
|
+
if (this.#state === CONNECTING)
|
|
94
|
+
throw new DOMException("The connection is not open yet", "InvalidStateError");
|
|
95
|
+
let size;
|
|
96
|
+
let message;
|
|
97
|
+
if (typeof data === "string") {
|
|
98
|
+
const bytes = encoder.encode(data);
|
|
99
|
+
size = bytes.length;
|
|
100
|
+
message = { type: "text", text: data };
|
|
101
|
+
} else if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
102
|
+
size = data.size;
|
|
103
|
+
message = data.arrayBuffer().then((b) => ({ type: "binary", data: toBase64(new Uint8Array(b)) }), () => ({ type: "skip" }));
|
|
104
|
+
} else {
|
|
105
|
+
const bytes = ArrayBuffer.isView(data) ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength) : new Uint8Array(data);
|
|
106
|
+
size = bytes.length;
|
|
107
|
+
message = { type: "binary", data: toBase64(bytes) };
|
|
108
|
+
}
|
|
109
|
+
this.#buffered += size;
|
|
110
|
+
if (this.#state !== OPEN)
|
|
111
|
+
return;
|
|
112
|
+
const settle = () => this.#buffered -= size;
|
|
113
|
+
this.#send(message).then(settle, settle);
|
|
114
|
+
}
|
|
115
|
+
close(code, reason) {
|
|
116
|
+
if (code !== undefined && code !== 1000 && !(code >= 3000 && code <= 4999)) {
|
|
117
|
+
throw new DOMException(`Invalid close code ${code}`, "InvalidAccessError");
|
|
118
|
+
}
|
|
119
|
+
const why = reason ?? "";
|
|
120
|
+
if (encoder.encode(why).length > 123)
|
|
121
|
+
throw new DOMException("The close reason is over 123 bytes", "SyntaxError");
|
|
122
|
+
if (this.#state === CLOSING || this.#state === CLOSED)
|
|
123
|
+
return;
|
|
124
|
+
if (this.#state === CONNECTING) {
|
|
125
|
+
this.#state = CLOSING;
|
|
126
|
+
this.#failed = "closed before the connection was established";
|
|
127
|
+
this.#events.close();
|
|
128
|
+
setTimeout(() => this.#finish(1006, "", false, this.#failed));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
this.#state = CLOSING;
|
|
132
|
+
this.#send({ type: "close", code: code ?? (why ? 1000 : 1005), reason: why }).catch(() => {});
|
|
133
|
+
}
|
|
134
|
+
#send(message) {
|
|
135
|
+
const seq = ++this.#seq;
|
|
136
|
+
return Promise.resolve(message).then((m) => call("plugin:websocket.Send", this.#id, seq, m));
|
|
137
|
+
}
|
|
138
|
+
#receive(e) {
|
|
139
|
+
switch (e.type) {
|
|
140
|
+
case "open":
|
|
141
|
+
if (this.#state !== CONNECTING)
|
|
142
|
+
return;
|
|
143
|
+
this.#state = OPEN;
|
|
144
|
+
this.#protocol = e.protocol ?? "";
|
|
145
|
+
this.dispatchEvent(new Event("open"));
|
|
146
|
+
return;
|
|
147
|
+
case "text":
|
|
148
|
+
case "binary": {
|
|
149
|
+
if (this.#state !== OPEN)
|
|
150
|
+
return;
|
|
151
|
+
let data = e.text ?? "";
|
|
152
|
+
if (e.type === "binary") {
|
|
153
|
+
const bytes = fromBase64(e.data ?? "");
|
|
154
|
+
data = this.binaryType === "arraybuffer" ? bytes.buffer : new Blob([bytes]);
|
|
155
|
+
}
|
|
156
|
+
this.dispatchEvent(new MessageEvent("message", { data, origin: new URL(this.url).origin }));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
case "close":
|
|
160
|
+
this.#finish(e.code ?? 1005, e.reason ?? "", e.clean ?? false, e.error);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
#finish(code, reason, wasClean, error) {
|
|
164
|
+
if (this.#state === CLOSED)
|
|
165
|
+
return;
|
|
166
|
+
this.#state = CLOSED;
|
|
167
|
+
this.#events.close();
|
|
168
|
+
if (error) {
|
|
169
|
+
console.error(`WebSocket connection to '${this.url}' failed: ${error}`);
|
|
170
|
+
this.dispatchEvent(new Event("error"));
|
|
171
|
+
}
|
|
172
|
+
this.dispatchEvent(closeEvent(code, reason, wasClean));
|
|
173
|
+
}
|
|
174
|
+
#handler(type) {
|
|
175
|
+
return this.#handlers.get(type)?.fn ?? null;
|
|
176
|
+
}
|
|
177
|
+
#setHandler(type, fn) {
|
|
178
|
+
let entry = this.#handlers.get(type);
|
|
179
|
+
if (!entry) {
|
|
180
|
+
const e = entry = { fn: null };
|
|
181
|
+
this.#handlers.set(type, e);
|
|
182
|
+
this.addEventListener(type, (ev) => e.fn?.call(this, ev));
|
|
183
|
+
}
|
|
184
|
+
entry.fn = typeof fn === "function" ? fn : null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
for (const [name, value] of Object.entries({ CONNECTING, OPEN, CLOSING, CLOSED })) {
|
|
188
|
+
Object.defineProperty(WebSocket.prototype, name, { value, enumerable: true });
|
|
189
|
+
}
|
|
190
|
+
var src_default = WebSocket;
|
|
191
|
+
function closeEvent(code, reason, wasClean) {
|
|
192
|
+
if (typeof CloseEvent !== "undefined")
|
|
193
|
+
return new CloseEvent("close", { code, reason, wasClean });
|
|
194
|
+
return Object.assign(new Event("close"), { code, reason, wasClean });
|
|
195
|
+
}
|
|
196
|
+
function failure(err) {
|
|
197
|
+
if (isCallError(err) && err.message.includes("plugin is not used")) {
|
|
198
|
+
return "the Go side does not use @mygo-plugins/websocket: call mygo.Use(websocket.Plugin) before App.Run";
|
|
199
|
+
}
|
|
200
|
+
return err instanceof Error ? err.message : String(err);
|
|
201
|
+
}
|
|
202
|
+
function toBase64(bytes) {
|
|
203
|
+
const native = bytes.toBase64;
|
|
204
|
+
if (native)
|
|
205
|
+
return native.call(bytes);
|
|
206
|
+
let s = "";
|
|
207
|
+
for (let i = 0;i < bytes.length; i += 32768) {
|
|
208
|
+
s += String.fromCharCode.apply(null, bytes.subarray(i, i + 32768));
|
|
209
|
+
}
|
|
210
|
+
return btoa(s);
|
|
211
|
+
}
|
|
212
|
+
function fromBase64(s) {
|
|
213
|
+
const native = Uint8Array.fromBase64;
|
|
214
|
+
if (native)
|
|
215
|
+
return native(s);
|
|
216
|
+
const bin = atob(s);
|
|
217
|
+
const bytes = new Uint8Array(bin.length);
|
|
218
|
+
for (let i = 0;i < bin.length; i++)
|
|
219
|
+
bytes[i] = bin.charCodeAt(i);
|
|
220
|
+
return bytes;
|
|
221
|
+
}
|
|
222
|
+
export {
|
|
223
|
+
WebSocket,
|
|
224
|
+
src_default as default
|
|
225
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mygo-plugins/websocket",
|
|
3
|
+
"version": "0.1.8",
|
|
4
|
+
"description": "A WebSocket whose connections Go makes, for MyGo desktop apps",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mygo",
|
|
7
|
+
"desktop",
|
|
8
|
+
"webview",
|
|
9
|
+
"go",
|
|
10
|
+
"websocket"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/egoist/mygo.git",
|
|
15
|
+
"directory": "plugins/websocket"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "bun run ../build.ts",
|
|
33
|
+
"test": "bun test"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"mygo-runtime": "^0.1.8"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"mygo-runtime": "^0.1.8"
|
|
40
|
+
}
|
|
41
|
+
}
|