@hashxltd/liveframe-vue 0.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 +2 -0
- package/bun.lock +78 -0
- package/package.json +23 -0
- package/src/client/index.ts +17 -0
- package/src/errors/index.ts +152 -0
- package/src/fabric/index.ts +172 -0
- package/src/fabric/systembus.ts +93 -0
- package/src/index.ts +1 -0
- package/src/protocol/codec.ts +177 -0
- package/src/protocol/constants.ts +125 -0
- package/src/protocol/index.ts +3 -0
- package/src/protocol/types.ts +255 -0
- package/src/transport/index.ts +212 -0
- package/src/utils/index.ts +157 -0
- package/tsconfig.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
# Liveframe Vue Client
|
|
2
|
+
A minimal-footprint TypeScript client for Vue applications designed to communicate with the Liveframe WebSocket server. It provides reactive event streams, channel-based subscriptions, and seamless integration with Vue reactivity and Pinia while supporting middleware-driven event processing and distributed presence updates.
|
package/bun.lock
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "liveframe-vue",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"nanoevents": "^9.1.0",
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@types/bun": "latest",
|
|
12
|
+
},
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"typescript": "^5",
|
|
15
|
+
"vue": "^3.5.30",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
"packages": {
|
|
20
|
+
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
|
21
|
+
|
|
22
|
+
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
|
23
|
+
|
|
24
|
+
"@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
|
25
|
+
|
|
26
|
+
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
|
27
|
+
|
|
28
|
+
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
|
29
|
+
|
|
30
|
+
"@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
|
|
31
|
+
|
|
32
|
+
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
|
33
|
+
|
|
34
|
+
"@vue/compiler-core": ["@vue/compiler-core@3.5.30", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.30", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw=="],
|
|
35
|
+
|
|
36
|
+
"@vue/compiler-dom": ["@vue/compiler-dom@3.5.30", "", { "dependencies": { "@vue/compiler-core": "3.5.30", "@vue/shared": "3.5.30" } }, "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g=="],
|
|
37
|
+
|
|
38
|
+
"@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.30", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/compiler-core": "3.5.30", "@vue/compiler-dom": "3.5.30", "@vue/compiler-ssr": "3.5.30", "@vue/shared": "3.5.30", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.8", "source-map-js": "^1.2.1" } }, "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A=="],
|
|
39
|
+
|
|
40
|
+
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.30", "", { "dependencies": { "@vue/compiler-dom": "3.5.30", "@vue/shared": "3.5.30" } }, "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA=="],
|
|
41
|
+
|
|
42
|
+
"@vue/reactivity": ["@vue/reactivity@3.5.30", "", { "dependencies": { "@vue/shared": "3.5.30" } }, "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q=="],
|
|
43
|
+
|
|
44
|
+
"@vue/runtime-core": ["@vue/runtime-core@3.5.30", "", { "dependencies": { "@vue/reactivity": "3.5.30", "@vue/shared": "3.5.30" } }, "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg=="],
|
|
45
|
+
|
|
46
|
+
"@vue/runtime-dom": ["@vue/runtime-dom@3.5.30", "", { "dependencies": { "@vue/reactivity": "3.5.30", "@vue/runtime-core": "3.5.30", "@vue/shared": "3.5.30", "csstype": "^3.2.3" } }, "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw=="],
|
|
47
|
+
|
|
48
|
+
"@vue/server-renderer": ["@vue/server-renderer@3.5.30", "", { "dependencies": { "@vue/compiler-ssr": "3.5.30", "@vue/shared": "3.5.30" }, "peerDependencies": { "vue": "3.5.30" } }, "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ=="],
|
|
49
|
+
|
|
50
|
+
"@vue/shared": ["@vue/shared@3.5.30", "", {}, "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ=="],
|
|
51
|
+
|
|
52
|
+
"bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
|
|
53
|
+
|
|
54
|
+
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
|
55
|
+
|
|
56
|
+
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
|
57
|
+
|
|
58
|
+
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
|
59
|
+
|
|
60
|
+
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
|
61
|
+
|
|
62
|
+
"nanoevents": ["nanoevents@9.1.0", "", {}, "sha512-Jd0fILWG44a9luj8v5kED4WI+zfkkgwKyRQKItTtlPfEsh7Lznfi1kr8/iZ+XAIss4Qq5GqRB0qtWbaz9ceO/A=="],
|
|
63
|
+
|
|
64
|
+
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
|
65
|
+
|
|
66
|
+
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
|
67
|
+
|
|
68
|
+
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
|
69
|
+
|
|
70
|
+
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
|
71
|
+
|
|
72
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
73
|
+
|
|
74
|
+
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
|
75
|
+
|
|
76
|
+
"vue": ["vue@3.5.30", "", { "dependencies": { "@vue/compiler-dom": "3.5.30", "@vue/compiler-sfc": "3.5.30", "@vue/runtime-dom": "3.5.30", "@vue/server-renderer": "3.5.30", "@vue/shared": "3.5.30" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg=="],
|
|
77
|
+
}
|
|
78
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hashxltd/liveframe-vue",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"module": "src/index.ts",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "bun build src/index.ts --outdir dist --target bun"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/bun": "latest"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"typescript": "^5",
|
|
18
|
+
"vue": "^3.5.30"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"nanoevents": "^9.1.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { LVFabric } from "../fabric";
|
|
2
|
+
|
|
3
|
+
export class LiveFrameClient {
|
|
4
|
+
private fabric: LVFabric;
|
|
5
|
+
|
|
6
|
+
constructor(url: string) {
|
|
7
|
+
this.fabric = new LVFabric(url);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
connect() {
|
|
11
|
+
this.fabric.open();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
disconnect() {
|
|
15
|
+
this.fabric.close();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// Typed error hierarchy for the entire client library.
|
|
3
|
+
// All thrown errors extend LiveFrameError so callers can discriminate with
|
|
4
|
+
// a single instanceof check.
|
|
5
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
// ─── Base ─────────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export class LiveFrameError extends Error {
|
|
10
|
+
constructor(
|
|
11
|
+
public readonly code: string,
|
|
12
|
+
message: string,
|
|
13
|
+
public override readonly cause?: unknown,
|
|
14
|
+
) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "LiveFrameError";
|
|
17
|
+
// Maintains proper prototype chain in transpiled ES5.
|
|
18
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ─── Frame-level errors (codec) ───────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
export enum FrameErrorCode {
|
|
25
|
+
MALFORMED_JSON = "MALFORMED_JSON",
|
|
26
|
+
INVALID_SHAPE = "INVALID_SHAPE",
|
|
27
|
+
VERSION_MISMATCH = "VERSION_MISMATCH",
|
|
28
|
+
MISSING_FIELD = "MISSING_FIELD",
|
|
29
|
+
INVALID_EVENT = "INVALID_EVENT",
|
|
30
|
+
PAYLOAD_TOO_LARGE = "PAYLOAD_TOO_LARGE",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class FrameError extends LiveFrameError {
|
|
34
|
+
constructor(code: FrameErrorCode, message: string) {
|
|
35
|
+
super(code, message);
|
|
36
|
+
this.name = "FrameError";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── Transport errors ─────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
export enum TransportErrorCode {
|
|
43
|
+
UPGRADE_FAILED = "UPGRADE_FAILED",
|
|
44
|
+
INSECURE_URL = "INSECURE_URL",
|
|
45
|
+
SOCKET_CLOSED = "SOCKET_CLOSED",
|
|
46
|
+
CONNECT_TIMEOUT = "CONNECT_TIMEOUT",
|
|
47
|
+
SEND_FAILED = "SEND_FAILED",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class TransportError extends LiveFrameError {
|
|
51
|
+
constructor(
|
|
52
|
+
code: TransportErrorCode,
|
|
53
|
+
message: string,
|
|
54
|
+
public readonly closeCode?: number,
|
|
55
|
+
) {
|
|
56
|
+
super(code, message);
|
|
57
|
+
this.name = "TransportError";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── Auth errors ──────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
export enum AuthErrorCode {
|
|
64
|
+
TOKEN_FETCH_FAILED = "TOKEN_FETCH_FAILED",
|
|
65
|
+
AUTH_TIMEOUT = "AUTH_TIMEOUT",
|
|
66
|
+
AUTH_REJECTED = "AUTH_REJECTED",
|
|
67
|
+
SESSION_EXPIRED = "SESSION_EXPIRED",
|
|
68
|
+
TOKEN_REFRESH_FAIL = "TOKEN_REFRESH_FAIL",
|
|
69
|
+
NOT_AUTHENTICATED = "NOT_AUTHENTICATED",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class AuthError extends LiveFrameError {
|
|
73
|
+
constructor(code: AuthErrorCode, message: string, cause?: unknown) {
|
|
74
|
+
super(code, message, cause);
|
|
75
|
+
this.name = "AuthError";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Rate-limit errors ────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
export enum RateLimitErrorCode {
|
|
82
|
+
CLIENT_LIMIT_EXCEEDED = "CLIENT_LIMIT_EXCEEDED",
|
|
83
|
+
SERVER_LIMIT_EXCEEDED = "SERVER_LIMIT_EXCEEDED",
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export class RateLimitError extends LiveFrameError {
|
|
87
|
+
constructor(
|
|
88
|
+
code: RateLimitErrorCode,
|
|
89
|
+
message: string,
|
|
90
|
+
public readonly retryAfterMs: number,
|
|
91
|
+
) {
|
|
92
|
+
super(code, message);
|
|
93
|
+
this.name = "RateLimitError";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Ack errors ───────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
export enum AckErrorCode {
|
|
100
|
+
TIMEOUT = "ACK_TIMEOUT",
|
|
101
|
+
SERVER_REJECTED = "ACK_SERVER_REJECTED",
|
|
102
|
+
CONNECTION_LOST = "ACK_CONNECTION_LOST",
|
|
103
|
+
QUEUE_FULL = "ACK_QUEUE_FULL",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class AckError extends LiveFrameError {
|
|
107
|
+
constructor(
|
|
108
|
+
code: AckErrorCode,
|
|
109
|
+
message: string,
|
|
110
|
+
public readonly refId: string,
|
|
111
|
+
/** Server-provided error code when the rejection came from ack.error */
|
|
112
|
+
public readonly serverCode?: string,
|
|
113
|
+
public readonly details?: ReadonlyArray<{ field: string; message: string }>,
|
|
114
|
+
) {
|
|
115
|
+
super(code, message);
|
|
116
|
+
this.name = "AckError";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── State errors ─────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
export enum StateErrorCode {
|
|
123
|
+
NOT_READY = "NOT_READY",
|
|
124
|
+
ALREADY_CONNECTED = "ALREADY_CONNECTED",
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export class StateError extends LiveFrameError {
|
|
128
|
+
constructor(code: StateErrorCode, message: string) {
|
|
129
|
+
super(code, message);
|
|
130
|
+
this.name = "StateError";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ─── Presence errors ──────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
export enum PresenceErrorCode {
|
|
137
|
+
SNAPSHOT_TIMEOUT = "SNAPSHOT_TIMEOUT",
|
|
138
|
+
SNAPSHOT_FAILED = "SNAPSHOT_FAILED",
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export class PresenceError extends LiveFrameError {
|
|
142
|
+
constructor(code: PresenceErrorCode, message: string) {
|
|
143
|
+
super(code, message);
|
|
144
|
+
this.name = "PresenceError";
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ─── Type guard ───────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
export function isLiveFrameError(e: unknown): e is LiveFrameError {
|
|
151
|
+
return e instanceof LiveFrameError;
|
|
152
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CloseCodes,
|
|
3
|
+
WS_SUBPROTOCOL,
|
|
4
|
+
isSystemEvent,
|
|
5
|
+
} from "../protocol/constants";
|
|
6
|
+
import { decode, encode } from "../protocol/codec";
|
|
7
|
+
import { TransportError, TransportErrorCode } from "../errors";
|
|
8
|
+
import {
|
|
9
|
+
SystemEventsBus,
|
|
10
|
+
SystemEventHandler,
|
|
11
|
+
type LFSystemEvents,
|
|
12
|
+
} from "./systembus";
|
|
13
|
+
import type { Envelope, SystemEvent } from "../protocol";
|
|
14
|
+
|
|
15
|
+
export class LVFabric {
|
|
16
|
+
private ws: WebSocket | null = null;
|
|
17
|
+
private _url: string;
|
|
18
|
+
private systemBus: SystemEventsBus;
|
|
19
|
+
|
|
20
|
+
constructor(url: string) {
|
|
21
|
+
this._url = this.validateUrl(url);
|
|
22
|
+
this.systemBus = new SystemEventsBus();
|
|
23
|
+
let _ = new SystemEventHandler(this.systemBus);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async open(connectionTimoutMS = 10_0000): Promise<void> {
|
|
27
|
+
if (this.ws && this.ws.readyState < WebSocket.CLOSING) return;
|
|
28
|
+
|
|
29
|
+
const _ws = new WebSocket(this._url, [WS_SUBPROTOCOL]);
|
|
30
|
+
_ws.binaryType = "arraybuffer";
|
|
31
|
+
|
|
32
|
+
const openPromise = new Promise<void>((resolve, reject) => {
|
|
33
|
+
// On Connection Open
|
|
34
|
+
_ws.onopen = () => {
|
|
35
|
+
this.ws = _ws;
|
|
36
|
+
|
|
37
|
+
// Protocl Checks
|
|
38
|
+
resolve();
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// On Frame Received
|
|
42
|
+
_ws.onmessage = (event) => {
|
|
43
|
+
this.handleIncoming(event.data);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// On Error
|
|
47
|
+
_ws.onerror = (event) => {
|
|
48
|
+
const err = new TransportError(
|
|
49
|
+
TransportErrorCode.UPGRADE_FAILED,
|
|
50
|
+
"Error while connecting to the LiveFrame Server.",
|
|
51
|
+
);
|
|
52
|
+
reject(err);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// On Close
|
|
56
|
+
_ws.onclose = (event) => {
|
|
57
|
+
this.ws = null;
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const timeoutPromise = new Promise<void>((_, reject) =>
|
|
62
|
+
setTimeout(() => {
|
|
63
|
+
this.ws?.close();
|
|
64
|
+
|
|
65
|
+
reject(
|
|
66
|
+
new TransportError(
|
|
67
|
+
TransportErrorCode.CONNECT_TIMEOUT,
|
|
68
|
+
`LiveFrame Server did not open within ${connectionTimoutMS}ms`,
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
}, connectionTimoutMS),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return Promise.race([openPromise, timeoutPromise]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─────────────────────────────────────────
|
|
78
|
+
// Send
|
|
79
|
+
// ─────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
send<P>(env: Envelope<P>): void {
|
|
82
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
83
|
+
throw new TransportError(
|
|
84
|
+
TransportErrorCode.SOCKET_CLOSED,
|
|
85
|
+
`Cannot send "${env.event}" — socket closed`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const frame = encode(env);
|
|
90
|
+
|
|
91
|
+
this.ws.send(frame);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─────────────────────────────────────────
|
|
95
|
+
// Close
|
|
96
|
+
// ─────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
close(code: number = CloseCodes.NORMAL, reason = ""): void {
|
|
99
|
+
if (this.ws && this.ws.readyState < WebSocket.CLOSING) {
|
|
100
|
+
this.ws.close(code, reason);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ─────────────────────────────────────────
|
|
105
|
+
// State
|
|
106
|
+
// ─────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
// get isOpen(): boolean {
|
|
109
|
+
// return this.ws?.readyState === WebSocket.OPEN ?? false;
|
|
110
|
+
// }
|
|
111
|
+
|
|
112
|
+
get bufferedAmount(): number {
|
|
113
|
+
return this.ws?.bufferedAmount ?? 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─── Private ────────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
// ─────────────────────────────────────────
|
|
119
|
+
// Incoming Frames
|
|
120
|
+
// ─────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
private handleIncoming(raw: string): void {
|
|
123
|
+
try {
|
|
124
|
+
const env = decode(raw);
|
|
125
|
+
|
|
126
|
+
if (isSystemEvent(env.event)) {
|
|
127
|
+
this.systemBus.emit(env.event as keyof LFSystemEvents, env as any);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
//this.events.emit("message", env);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.log(err);
|
|
134
|
+
//this.log.warn("[transport] malformed frame dropped", { err });
|
|
135
|
+
//this.events.emit("error", err as Error);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private validateUrl(url: string): string {
|
|
140
|
+
let parsed: URL;
|
|
141
|
+
try {
|
|
142
|
+
parsed = new URL(url);
|
|
143
|
+
} catch {
|
|
144
|
+
throw new TransportError(
|
|
145
|
+
TransportErrorCode.UPGRADE_FAILED,
|
|
146
|
+
`Invalid WebSocket URL: "${url}"`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (parsed.protocol === "http:") {
|
|
151
|
+
// Rewrite http: → ws: so consumers can pass HTTP URLs conveniently.
|
|
152
|
+
parsed.protocol = "ws:";
|
|
153
|
+
} else if (parsed.protocol === "https:") {
|
|
154
|
+
parsed.protocol = "wss:";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const secure = parsed.protocol === "wss:";
|
|
158
|
+
|
|
159
|
+
// Block plaintext connections when the page itself is HTTPS.
|
|
160
|
+
const pageIsSecure =
|
|
161
|
+
typeof location !== "undefined" && location.protocol === "https:";
|
|
162
|
+
|
|
163
|
+
if (!secure && pageIsSecure) {
|
|
164
|
+
throw new TransportError(
|
|
165
|
+
TransportErrorCode.INSECURE_URL,
|
|
166
|
+
`Insecure ws:// connection blocked on an HTTPS origin. Use wss://.`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return parsed.toString();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createNanoEvents } from "nanoevents";
|
|
2
|
+
import {
|
|
3
|
+
isSystemEvent,
|
|
4
|
+
SystemEvents,
|
|
5
|
+
type SystemEvent,
|
|
6
|
+
} from "../protocol/constants";
|
|
7
|
+
import {
|
|
8
|
+
decode,
|
|
9
|
+
type SysConnectedEnvelope,
|
|
10
|
+
type SysPingEnvelope,
|
|
11
|
+
type SysPongEnvelope,
|
|
12
|
+
type SysDisconnectEnvelope,
|
|
13
|
+
type SysRateLimitedEnvelope,
|
|
14
|
+
type AuthOkEnvelope,
|
|
15
|
+
type AuthRevokedEnvelope,
|
|
16
|
+
type AuthLoginPayload,
|
|
17
|
+
type AuthRefreshPayload,
|
|
18
|
+
type Envelope,
|
|
19
|
+
type AckOkEnvelope,
|
|
20
|
+
type AckErrorEnvelope,
|
|
21
|
+
type PresenceInfo,
|
|
22
|
+
type ErrorPayload,
|
|
23
|
+
type PresenceLeftPayload,
|
|
24
|
+
type PresenceUpdatePayload,
|
|
25
|
+
type PresenceListPayload,
|
|
26
|
+
type PresenceSnapshotPayload,
|
|
27
|
+
} from "../protocol";
|
|
28
|
+
|
|
29
|
+
interface LFSystemPayloadEnvelopes {
|
|
30
|
+
[SystemEvents.CONNECTED]: SysConnectedEnvelope;
|
|
31
|
+
[SystemEvents.PING]: SysPingEnvelope;
|
|
32
|
+
[SystemEvents.PONG]: SysPongEnvelope;
|
|
33
|
+
[SystemEvents.DISCONNECT]: SysDisconnectEnvelope;
|
|
34
|
+
[SystemEvents.RATE_LIMITED]: SysRateLimitedEnvelope;
|
|
35
|
+
|
|
36
|
+
[SystemEvents.AUTH_OK]: AuthOkEnvelope;
|
|
37
|
+
[SystemEvents.AUTH_REVOKED]: AuthRevokedEnvelope;
|
|
38
|
+
[SystemEvents.AUTH_LOGIN]: Envelope<AuthLoginPayload>;
|
|
39
|
+
[SystemEvents.AUTH_REFRESH]: Envelope<AuthRefreshPayload>;
|
|
40
|
+
|
|
41
|
+
[SystemEvents.ACK_OK]: AckOkEnvelope;
|
|
42
|
+
[SystemEvents.ACK_ERROR]: AckErrorEnvelope;
|
|
43
|
+
|
|
44
|
+
[SystemEvents.PRESENCE_JOINED]: Envelope<PresenceInfo>;
|
|
45
|
+
[SystemEvents.PRESENCE_LEFT]: Envelope<PresenceLeftPayload>;
|
|
46
|
+
[SystemEvents.PRESENCE_UPDATE]: Envelope<PresenceUpdatePayload>;
|
|
47
|
+
[SystemEvents.PRESENCE_LIST]: Envelope<PresenceListPayload>;
|
|
48
|
+
[SystemEvents.PRESENCE_SNAPSHOT]: Envelope<PresenceSnapshotPayload>;
|
|
49
|
+
|
|
50
|
+
[SystemEvents.ERROR_AUTH]: Envelope<ErrorPayload>;
|
|
51
|
+
[SystemEvents.ERROR_VALIDATION]: Envelope<ErrorPayload>;
|
|
52
|
+
[SystemEvents.ERROR_ROUTING]: Envelope<ErrorPayload>;
|
|
53
|
+
[SystemEvents.ERROR_RATE_LIMIT]: Envelope<ErrorPayload>;
|
|
54
|
+
[SystemEvents.ERROR_INTERNAL]: Envelope<ErrorPayload>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type LFSystemEvents = {
|
|
58
|
+
[K in keyof LFSystemPayloadEnvelopes]: (
|
|
59
|
+
data: LFSystemPayloadEnvelopes[K],
|
|
60
|
+
) => void;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export class SystemEventsBus {
|
|
64
|
+
private emmiter = createNanoEvents<LFSystemEvents>();
|
|
65
|
+
|
|
66
|
+
on<K extends keyof LFSystemEvents>(
|
|
67
|
+
event: K,
|
|
68
|
+
listener: LFSystemEvents[K],
|
|
69
|
+
): () => void {
|
|
70
|
+
return this.emmiter.on(event, listener);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
emit<K extends keyof LFSystemEvents>(
|
|
74
|
+
event: K,
|
|
75
|
+
data: LFSystemPayloadEnvelopes[K],
|
|
76
|
+
): void {
|
|
77
|
+
(this.emmiter as any).emit(event, data);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class SystemEventHandler {
|
|
82
|
+
constructor(private bus: SystemEventsBus) {
|
|
83
|
+
this.registerListeners();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private registerListeners() {
|
|
87
|
+
this.bus.on(SystemEvents.CONNECTED, this.handleConnected);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private handleConnected(data: SysConnectedEnvelope) {
|
|
91
|
+
console.log("Connected", data);
|
|
92
|
+
}
|
|
93
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./client";
|