@effectionx/websocket 2.0.1

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/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # WebSocket
2
+
3
+ A streamlined [WebSocket][websocket] client for Effection programs that
4
+ transforms the event-based WebSocket API into a clean, resource-oriented stream.
5
+
6
+ ## Why Use this API?
7
+
8
+ Traditional WebSocket API require managing multiple event handlers (`open`,
9
+ `close`, `error`, `message`) which can become complex and error-prone.
10
+
11
+ This package simplifies WebSocket usage by:
12
+
13
+ - Providing a clean stream-based interface
14
+ - Handling connection state management automatically
15
+ - Implementing proper error handling
16
+ - Ensuring resource cleanup
17
+
18
+ ## Basic Usage
19
+
20
+ ```typescript
21
+ import { each, main } from "effection";
22
+ import { useWebSocket } from "@effectionx/websocket";
23
+
24
+ await main(function* () {
25
+ // Connection is guaranteed to be open when this returns
26
+ let socket = yield* useWebSocket("ws://websocket.example.org");
27
+
28
+ // Send messages to the server
29
+ socket.send("Hello World");
30
+
31
+ // Receive messages using a simple iterator
32
+ for (let message of yield* each(socket)) {
33
+ console.log("Message from server", message);
34
+ yield* each.next();
35
+ }
36
+ });
37
+ ```
38
+
39
+ ## Features
40
+
41
+ - **Ready-to-use Connections**: `useWebSocket()` returns only after the
42
+ connection is established
43
+ - **Automatic Error Handling**: Socket errors are properly propagated to your
44
+ error boundary
45
+ - **Stream-based API**: Messages are delivered through a simple stream interface
46
+ - **Clean Resource Management**: Connections are properly cleaned up when the
47
+ operation completes
48
+
49
+ ## Advanced Usage
50
+
51
+ ### Custom WebSocket Implementations
52
+
53
+ For environments without native WebSocket support (like Node.js < 21), you can
54
+ provide your own WebSocket implementation:
55
+
56
+ ```typescript
57
+ import { createWebSocket } from "my-websocket-client";
58
+ import { each, main } from "effection";
59
+ import { useWebSocket } from "@effectionx/websocket";
60
+
61
+ await main(function* () {
62
+ let socket = yield* useWebSocket(() =>
63
+ createWebSocket("ws://websocket.example.org")
64
+ );
65
+
66
+ for (let message of yield* each(socket)) {
67
+ console.log("Message from server", message);
68
+ yield* each.next();
69
+ }
70
+ });
71
+ ```
72
+
73
+ [websocket]: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
package/esm/mod.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./websocket.js";
2
+ //# sourceMappingURL=mod.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC"}
package/esm/mod.js ADDED
@@ -0,0 +1 @@
1
+ export * from "./websocket.js";
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,90 @@
1
+ import type { Operation, Stream } from "effection";
2
+ /**
3
+ * Handle to a
4
+ * [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object
5
+ * that can be consumed as an Effection stream. It has all the same properties as
6
+ * the underlying `WebSocket` apart from the event handlers. Instead, the resource
7
+ * itself is a subscribale stream. When the socket is closed, the stream will
8
+ * complete with a [`CloseEvent`](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent)
9
+ *
10
+ * A WebSocketResource does not have an explicit close method. Rather, the underlying
11
+ * socket will be automatically closed when the resource passes out of scope.
12
+ */
13
+ export interface WebSocketResource<T> extends Stream<MessageEvent<T>, CloseEvent> {
14
+ /**
15
+ * the type of data that this websocket accepts
16
+ */
17
+ readonly binaryType: BinaryType;
18
+ readonly bufferedAmmount: number;
19
+ readonly extensions: string;
20
+ readonly protocol: string;
21
+ readonly readyState: number;
22
+ readonly url: string;
23
+ send(data: WebSocketData): void;
24
+ }
25
+ /**
26
+ * Create a [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
27
+ * resource using the native
28
+ * [WebSocket constructor](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket)
29
+ * available on the current platform.
30
+ *
31
+ * The resource will not be returned until a connection has been
32
+ * succesffuly established with the server and the
33
+ * [`open`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/open_event)
34
+ * has been received. Once initialized, it will crash if it receives
35
+ * an [`error`]() event at any time.
36
+ *
37
+ * Once created, the websocket resource can be use to consume events from the server:
38
+ *
39
+ * ```ts
40
+ * let socket = yield* useWebSocket("ws://websocket.example.org");
41
+ *
42
+ * for (let event of yield* each(socket)) {
43
+ * console.log('event data: ', event.data);
44
+ * yield* each.next();
45
+ * }
46
+ * ```
47
+ *
48
+ * @param url - The URL of the target WebSocket server to connect to. The URL must use one of the following schemes: ws, wss, http, or https, and cannot include a URL fragment. If a relative URL is provided, it is relative to the base URL of the calling script. For more detail, see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#url
49
+ *
50
+ * @param prototol - A single string or an array of strings representing the sub-protocol(s) that the client would like to use, in order of preference. If it is omitted, an empty array is used by default, i.e. []. For more details, see
51
+ *
52
+ * @returns an operation yielding a {@link WebSocketResource}
53
+ */
54
+ export declare function useWebSocket<T>(url: string, protocols?: string): Operation<WebSocketResource<T>>;
55
+ /**
56
+ * Create a [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
57
+ * resource, but delegate the creation of the underlying websocket to a function
58
+ * of your choice. This is necessary on platforms that do not have a global
59
+ * `WebSocket` constructor such as NodeJS \<= 20.
60
+ *
61
+ * The resource will not be returned until a connection has been
62
+ * succesffuly established with the server and the
63
+ * [`open`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/open_event)
64
+ * has been received. Once initialized, it will crash if it receives
65
+ * an [`error`]() event at any time.
66
+ *
67
+ * Once created, the websocket resource can be use to consume events from the server:
68
+ *
69
+ * ```ts
70
+ * import * as ws from 'ws';
71
+ *
72
+ * function* example() {
73
+ * let socket = yield* useWebSocket(() => new ws.WebSocket("ws://websocket.example.org"));
74
+ *
75
+ * for (let event of yield* each(socket)) {
76
+ * console.log('event data: ', event.data);
77
+ * yield* each.next();
78
+ * }
79
+ * }
80
+ *
81
+ * ```
82
+ * @param create - a function that will construct the underlying [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object that this resource wil use
83
+ * @returns an operation yielding a {@link WebSocketResource}
84
+ */
85
+ export declare function useWebSocket<T>(create: () => WebSocket): Operation<WebSocketResource<T>>;
86
+ /**
87
+ * @ignore
88
+ */
89
+ export type WebSocketData = Parameters<WebSocket["send"]>[0];
90
+ //# sourceMappingURL=websocket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAEnD;;;;;;;;;;GAUG;AACH,MAAM,WAAW,iBAAiB,CAAC,CAAC,CAClC,SAAQ,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC;IAC3C;;OAEG;IACH,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAC5B,GAAG,EAAE,MAAM,EACX,SAAS,CAAC,EAAE,MAAM,GACjB,SAAS,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC;AAEnC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAC5B,MAAM,EAAE,MAAM,SAAS,GACtB,SAAS,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC;AAsEnC;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC"}
@@ -0,0 +1,60 @@
1
+ import { createSignal, once, race, resource, spawn, withResolvers, } from "effection";
2
+ /**
3
+ * @ignore the catch-all version that supports both forms above.
4
+ */
5
+ export function useWebSocket(url, protocols) {
6
+ return resource(function* (provide) {
7
+ let socket = typeof url === "string"
8
+ ? new WebSocket(url, protocols)
9
+ : url();
10
+ let messages = createSignal();
11
+ let { operation: closed, resolve: close } = withResolvers();
12
+ yield* spawn(function* () {
13
+ throw yield* once(socket, "error");
14
+ });
15
+ yield* once(socket, "open");
16
+ yield* spawn(function* () {
17
+ let subscription = yield* messages;
18
+ let next = yield* subscription.next();
19
+ while (!next.done) {
20
+ next = yield* subscription.next();
21
+ }
22
+ close(next.value);
23
+ });
24
+ try {
25
+ socket.addEventListener("message", messages.send);
26
+ socket.addEventListener("close", messages.close);
27
+ yield* race([
28
+ closed,
29
+ provide({
30
+ get binaryType() {
31
+ return socket.binaryType;
32
+ },
33
+ get bufferedAmmount() {
34
+ return socket.bufferedAmount;
35
+ },
36
+ get extensions() {
37
+ return socket.extensions;
38
+ },
39
+ get protocol() {
40
+ return socket.protocol;
41
+ },
42
+ get readyState() {
43
+ return socket.readyState;
44
+ },
45
+ get url() {
46
+ return socket.url;
47
+ },
48
+ send: (data) => socket.send(data),
49
+ [Symbol.iterator]: messages[Symbol.iterator],
50
+ }),
51
+ ]);
52
+ }
53
+ finally {
54
+ socket.close(1000, "released");
55
+ yield* closed;
56
+ socket.removeEventListener("message", messages.send);
57
+ socket.removeEventListener("close", messages.close);
58
+ }
59
+ });
60
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@effectionx/websocket",
3
+ "version": "2.0.1",
4
+ "author": "engineering@frontside.com",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/thefrontside/effectionx.git"
8
+ },
9
+ "license": "MIT",
10
+ "bugs": {
11
+ "url": "https://github.com/thefrontside/effectionx/issues"
12
+ },
13
+ "main": "./script/mod.js",
14
+ "module": "./esm/mod.js",
15
+ "exports": {
16
+ ".": {
17
+ "import": "./esm/mod.js",
18
+ "require": "./script/mod.js"
19
+ }
20
+ },
21
+ "engines": {
22
+ "node": ">= 16"
23
+ },
24
+ "sideEffects": false,
25
+ "dependencies": {
26
+ "effection": "4.0.0-alpha.6"
27
+ },
28
+ "_generatedBy": "dnt@dev"
29
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./websocket.js";
2
+ //# sourceMappingURL=mod.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC"}
package/script/mod.js ADDED
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./websocket.js"), exports);
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,90 @@
1
+ import type { Operation, Stream } from "effection";
2
+ /**
3
+ * Handle to a
4
+ * [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object
5
+ * that can be consumed as an Effection stream. It has all the same properties as
6
+ * the underlying `WebSocket` apart from the event handlers. Instead, the resource
7
+ * itself is a subscribale stream. When the socket is closed, the stream will
8
+ * complete with a [`CloseEvent`](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent)
9
+ *
10
+ * A WebSocketResource does not have an explicit close method. Rather, the underlying
11
+ * socket will be automatically closed when the resource passes out of scope.
12
+ */
13
+ export interface WebSocketResource<T> extends Stream<MessageEvent<T>, CloseEvent> {
14
+ /**
15
+ * the type of data that this websocket accepts
16
+ */
17
+ readonly binaryType: BinaryType;
18
+ readonly bufferedAmmount: number;
19
+ readonly extensions: string;
20
+ readonly protocol: string;
21
+ readonly readyState: number;
22
+ readonly url: string;
23
+ send(data: WebSocketData): void;
24
+ }
25
+ /**
26
+ * Create a [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
27
+ * resource using the native
28
+ * [WebSocket constructor](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket)
29
+ * available on the current platform.
30
+ *
31
+ * The resource will not be returned until a connection has been
32
+ * succesffuly established with the server and the
33
+ * [`open`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/open_event)
34
+ * has been received. Once initialized, it will crash if it receives
35
+ * an [`error`]() event at any time.
36
+ *
37
+ * Once created, the websocket resource can be use to consume events from the server:
38
+ *
39
+ * ```ts
40
+ * let socket = yield* useWebSocket("ws://websocket.example.org");
41
+ *
42
+ * for (let event of yield* each(socket)) {
43
+ * console.log('event data: ', event.data);
44
+ * yield* each.next();
45
+ * }
46
+ * ```
47
+ *
48
+ * @param url - The URL of the target WebSocket server to connect to. The URL must use one of the following schemes: ws, wss, http, or https, and cannot include a URL fragment. If a relative URL is provided, it is relative to the base URL of the calling script. For more detail, see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#url
49
+ *
50
+ * @param prototol - A single string or an array of strings representing the sub-protocol(s) that the client would like to use, in order of preference. If it is omitted, an empty array is used by default, i.e. []. For more details, see
51
+ *
52
+ * @returns an operation yielding a {@link WebSocketResource}
53
+ */
54
+ export declare function useWebSocket<T>(url: string, protocols?: string): Operation<WebSocketResource<T>>;
55
+ /**
56
+ * Create a [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
57
+ * resource, but delegate the creation of the underlying websocket to a function
58
+ * of your choice. This is necessary on platforms that do not have a global
59
+ * `WebSocket` constructor such as NodeJS \<= 20.
60
+ *
61
+ * The resource will not be returned until a connection has been
62
+ * succesffuly established with the server and the
63
+ * [`open`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/open_event)
64
+ * has been received. Once initialized, it will crash if it receives
65
+ * an [`error`]() event at any time.
66
+ *
67
+ * Once created, the websocket resource can be use to consume events from the server:
68
+ *
69
+ * ```ts
70
+ * import * as ws from 'ws';
71
+ *
72
+ * function* example() {
73
+ * let socket = yield* useWebSocket(() => new ws.WebSocket("ws://websocket.example.org"));
74
+ *
75
+ * for (let event of yield* each(socket)) {
76
+ * console.log('event data: ', event.data);
77
+ * yield* each.next();
78
+ * }
79
+ * }
80
+ *
81
+ * ```
82
+ * @param create - a function that will construct the underlying [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object that this resource wil use
83
+ * @returns an operation yielding a {@link WebSocketResource}
84
+ */
85
+ export declare function useWebSocket<T>(create: () => WebSocket): Operation<WebSocketResource<T>>;
86
+ /**
87
+ * @ignore
88
+ */
89
+ export type WebSocketData = Parameters<WebSocket["send"]>[0];
90
+ //# sourceMappingURL=websocket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAEnD;;;;;;;;;;GAUG;AACH,MAAM,WAAW,iBAAiB,CAAC,CAAC,CAClC,SAAQ,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC;IAC3C;;OAEG;IACH,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAC5B,GAAG,EAAE,MAAM,EACX,SAAS,CAAC,EAAE,MAAM,GACjB,SAAS,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC;AAEnC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAC5B,MAAM,EAAE,MAAM,SAAS,GACtB,SAAS,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC;AAsEnC;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC"}
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useWebSocket = useWebSocket;
4
+ const effection_1 = require("effection");
5
+ /**
6
+ * @ignore the catch-all version that supports both forms above.
7
+ */
8
+ function useWebSocket(url, protocols) {
9
+ return (0, effection_1.resource)(function* (provide) {
10
+ let socket = typeof url === "string"
11
+ ? new WebSocket(url, protocols)
12
+ : url();
13
+ let messages = (0, effection_1.createSignal)();
14
+ let { operation: closed, resolve: close } = (0, effection_1.withResolvers)();
15
+ yield* (0, effection_1.spawn)(function* () {
16
+ throw yield* (0, effection_1.once)(socket, "error");
17
+ });
18
+ yield* (0, effection_1.once)(socket, "open");
19
+ yield* (0, effection_1.spawn)(function* () {
20
+ let subscription = yield* messages;
21
+ let next = yield* subscription.next();
22
+ while (!next.done) {
23
+ next = yield* subscription.next();
24
+ }
25
+ close(next.value);
26
+ });
27
+ try {
28
+ socket.addEventListener("message", messages.send);
29
+ socket.addEventListener("close", messages.close);
30
+ yield* (0, effection_1.race)([
31
+ closed,
32
+ provide({
33
+ get binaryType() {
34
+ return socket.binaryType;
35
+ },
36
+ get bufferedAmmount() {
37
+ return socket.bufferedAmount;
38
+ },
39
+ get extensions() {
40
+ return socket.extensions;
41
+ },
42
+ get protocol() {
43
+ return socket.protocol;
44
+ },
45
+ get readyState() {
46
+ return socket.readyState;
47
+ },
48
+ get url() {
49
+ return socket.url;
50
+ },
51
+ send: (data) => socket.send(data),
52
+ [Symbol.iterator]: messages[Symbol.iterator],
53
+ }),
54
+ ]);
55
+ }
56
+ finally {
57
+ socket.close(1000, "released");
58
+ yield* closed;
59
+ socket.removeEventListener("message", messages.send);
60
+ socket.removeEventListener("close", messages.close);
61
+ }
62
+ });
63
+ }