@sudo-ping-pong/prism-expo-client 0.1.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/LICENSE +21 -0
- package/README.md +182 -0
- package/dist/buffer.d.ts +8 -0
- package/dist/buffer.d.ts.map +1 -0
- package/dist/buffer.js +19 -0
- package/dist/buffer.js.map +1 -0
- package/dist/capture.d.ts +5 -0
- package/dist/capture.d.ts.map +1 -0
- package/dist/capture.js +36 -0
- package/dist/capture.js.map +1 -0
- package/dist/command-handler.d.ts +2 -0
- package/dist/command-handler.d.ts.map +1 -0
- package/dist/command-handler.js +14 -0
- package/dist/command-handler.js.map +1 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +15 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +103 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/redux.d.ts +28 -0
- package/dist/middleware/redux.d.ts.map +1 -0
- package/dist/middleware/redux.js +94 -0
- package/dist/middleware/redux.js.map +1 -0
- package/dist/middleware/zustand.d.ts +11 -0
- package/dist/middleware/zustand.d.ts.map +1 -0
- package/dist/middleware/zustand.js +37 -0
- package/dist/middleware/zustand.js.map +1 -0
- package/dist/patches/axios.d.ts +4 -0
- package/dist/patches/axios.d.ts.map +1 -0
- package/dist/patches/axios.js +140 -0
- package/dist/patches/axios.js.map +1 -0
- package/dist/patches/capture-guard.d.ts +5 -0
- package/dist/patches/capture-guard.d.ts.map +1 -0
- package/dist/patches/capture-guard.js +24 -0
- package/dist/patches/capture-guard.js.map +1 -0
- package/dist/patches/console.d.ts +2 -0
- package/dist/patches/console.d.ts.map +1 -0
- package/dist/patches/console.js +21 -0
- package/dist/patches/console.js.map +1 -0
- package/dist/patches/fetch.d.ts +4 -0
- package/dist/patches/fetch.d.ts.map +1 -0
- package/dist/patches/fetch.js +106 -0
- package/dist/patches/fetch.js.map +1 -0
- package/dist/patches/xhr.d.ts +2 -0
- package/dist/patches/xhr.d.ts.map +1 -0
- package/dist/patches/xhr.js +74 -0
- package/dist/patches/xhr.js.map +1 -0
- package/dist/profiler.d.ts +9 -0
- package/dist/profiler.d.ts.map +1 -0
- package/dist/profiler.js +19 -0
- package/dist/profiler.js.map +1 -0
- package/dist/transport-registry.d.ts +8 -0
- package/dist/transport-registry.d.ts.map +1 -0
- package/dist/transport-registry.js +11 -0
- package/dist/transport-registry.js.map +1 -0
- package/dist/transport.d.ts +38 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +153 -0
- package/dist/transport.js.map +1 -0
- package/package.json +111 -0
- package/src/buffer.ts +23 -0
- package/src/capture.ts +40 -0
- package/src/command-handler.ts +15 -0
- package/src/config.ts +29 -0
- package/src/env.d.ts +1 -0
- package/src/index.ts +148 -0
- package/src/middleware/redux.ts +126 -0
- package/src/middleware/zustand.ts +58 -0
- package/src/patches/axios.ts +182 -0
- package/src/patches/capture-guard.ts +24 -0
- package/src/patches/console.ts +27 -0
- package/src/patches/fetch.ts +118 -0
- package/src/patches/xhr.ts +103 -0
- package/src/profiler.tsx +36 -0
- package/src/transport-registry.ts +20 -0
- package/src/transport.ts +198 -0
package/src/transport.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { PrismEnvelope, PrismHandshakeAck } from '@sudo-ping-pong/prism-protocol';
|
|
2
|
+
import { isPrismCommand } from '@sudo-ping-pong/prism-protocol';
|
|
3
|
+
import { EventBuffer } from './buffer';
|
|
4
|
+
import { handlePrismCommand } from './command-handler';
|
|
5
|
+
|
|
6
|
+
export interface TransportOptions {
|
|
7
|
+
host: string;
|
|
8
|
+
port: number;
|
|
9
|
+
onConnect?: () => void;
|
|
10
|
+
onDisconnect?: () => void;
|
|
11
|
+
onError?: (error: PrismTransportError) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type PrismTransportError =
|
|
15
|
+
| { type: 'auth'; message: string }
|
|
16
|
+
| { type: 'network'; message: string }
|
|
17
|
+
| { type: 'unavailable'; message: string };
|
|
18
|
+
|
|
19
|
+
type WebSocketLike = {
|
|
20
|
+
send(data: string): void;
|
|
21
|
+
close(): void;
|
|
22
|
+
readyState: number;
|
|
23
|
+
onopen: ((ev: unknown) => void) | null;
|
|
24
|
+
onclose: ((ev: unknown) => void) | null;
|
|
25
|
+
onerror: ((ev: unknown) => void) | null;
|
|
26
|
+
onmessage: ((ev: { data: string }) => void) | null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const WS_OPEN = 1;
|
|
30
|
+
const MAX_BACKOFF_MS = 30_000;
|
|
31
|
+
|
|
32
|
+
export class PrismTransport {
|
|
33
|
+
private ws: WebSocketLike | null = null;
|
|
34
|
+
private readonly buffer = new EventBuffer();
|
|
35
|
+
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
36
|
+
private readonly options: TransportOptions;
|
|
37
|
+
private connected = false;
|
|
38
|
+
private destroyed = false;
|
|
39
|
+
private authFailed = false;
|
|
40
|
+
private reconnectAttempts = 0;
|
|
41
|
+
|
|
42
|
+
constructor(options: TransportOptions) {
|
|
43
|
+
this.options = options;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
connect(): void {
|
|
47
|
+
if (this.destroyed || this.authFailed) return;
|
|
48
|
+
|
|
49
|
+
const url = `ws://${this.options.host}:${this.options.port}`;
|
|
50
|
+
const WS = getWebSocketConstructor();
|
|
51
|
+
if (!WS) {
|
|
52
|
+
this.options.onError?.({
|
|
53
|
+
type: 'unavailable',
|
|
54
|
+
message: 'WebSocket is not available in this environment',
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (this.ws) {
|
|
60
|
+
this.ws.onclose = null;
|
|
61
|
+
this.ws.onerror = null;
|
|
62
|
+
this.ws.onmessage = null;
|
|
63
|
+
this.ws.close();
|
|
64
|
+
this.ws = null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
this.ws = new WS(url) as WebSocketLike;
|
|
68
|
+
|
|
69
|
+
this.ws.onopen = () => {
|
|
70
|
+
this.reconnectAttempts = 0;
|
|
71
|
+
this.ws?.send(
|
|
72
|
+
JSON.stringify({
|
|
73
|
+
type: 'handshake',
|
|
74
|
+
role: 'sdk',
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
this.ws.onmessage = (ev) => {
|
|
80
|
+
try {
|
|
81
|
+
const msg = JSON.parse(ev.data) as PrismHandshakeAck | unknown;
|
|
82
|
+
|
|
83
|
+
if (
|
|
84
|
+
typeof msg === 'object' &&
|
|
85
|
+
msg !== null &&
|
|
86
|
+
'type' in msg &&
|
|
87
|
+
(msg as PrismHandshakeAck).type === 'handshake_ack'
|
|
88
|
+
) {
|
|
89
|
+
const ack = msg as PrismHandshakeAck;
|
|
90
|
+
if (ack.success) {
|
|
91
|
+
this.connected = true;
|
|
92
|
+
this.options.onConnect?.();
|
|
93
|
+
this.flushBuffer();
|
|
94
|
+
} else {
|
|
95
|
+
this.authFailed = true;
|
|
96
|
+
const message = ack.message ?? 'Handshake rejected by server';
|
|
97
|
+
this.options.onError?.({ type: 'auth', message });
|
|
98
|
+
this.ws?.close();
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (this.connected && isPrismCommand(msg)) {
|
|
104
|
+
handlePrismCommand(msg);
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
// ignore malformed messages
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
this.ws.onclose = () => {
|
|
112
|
+
this.connected = false;
|
|
113
|
+
this.ws = null;
|
|
114
|
+
|
|
115
|
+
if (this.destroyed || this.authFailed) return;
|
|
116
|
+
|
|
117
|
+
this.options.onDisconnect?.();
|
|
118
|
+
this.scheduleReconnect();
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
this.ws.onerror = () => {
|
|
122
|
+
this.connected = false;
|
|
123
|
+
this.options.onError?.({
|
|
124
|
+
type: 'network',
|
|
125
|
+
message: `Cannot reach Prism server at ${this.options.host}:${this.options.port}`,
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
send(envelope: PrismEnvelope): void {
|
|
131
|
+
queueMicrotask(() => {
|
|
132
|
+
if (this.connected && this.ws?.readyState === WS_OPEN) {
|
|
133
|
+
try {
|
|
134
|
+
this.ws.send(JSON.stringify(envelope));
|
|
135
|
+
} catch {
|
|
136
|
+
this.buffer.push(envelope);
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
this.buffer.push(envelope);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
disconnect(): void {
|
|
145
|
+
this.destroyed = true;
|
|
146
|
+
if (this.reconnectTimer) {
|
|
147
|
+
clearTimeout(this.reconnectTimer);
|
|
148
|
+
this.reconnectTimer = null;
|
|
149
|
+
}
|
|
150
|
+
if (this.ws) {
|
|
151
|
+
this.ws.onclose = null;
|
|
152
|
+
this.ws.onerror = null;
|
|
153
|
+
this.ws.close();
|
|
154
|
+
this.ws = null;
|
|
155
|
+
}
|
|
156
|
+
this.connected = false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
resetAuthFailure(): void {
|
|
160
|
+
this.authFailed = false;
|
|
161
|
+
this.destroyed = false;
|
|
162
|
+
this.reconnectAttempts = 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
get isConnected(): boolean {
|
|
166
|
+
return this.connected;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
get hasAuthFailed(): boolean {
|
|
170
|
+
return this.authFailed;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private flushBuffer(): void {
|
|
174
|
+
const pending = this.buffer.drain();
|
|
175
|
+
for (const envelope of pending) {
|
|
176
|
+
this.send(envelope);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private scheduleReconnect(): void {
|
|
181
|
+
if (this.destroyed || this.authFailed || this.reconnectTimer) return;
|
|
182
|
+
|
|
183
|
+
const delay = Math.min(3000 * 2 ** this.reconnectAttempts, MAX_BACKOFF_MS);
|
|
184
|
+
this.reconnectAttempts++;
|
|
185
|
+
|
|
186
|
+
this.reconnectTimer = setTimeout(() => {
|
|
187
|
+
this.reconnectTimer = null;
|
|
188
|
+
this.connect();
|
|
189
|
+
}, delay);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function getWebSocketConstructor(): (new (url: string) => WebSocketLike) | null {
|
|
194
|
+
if (typeof globalThis.WebSocket !== 'undefined') {
|
|
195
|
+
return globalThis.WebSocket as unknown as new (url: string) => WebSocketLike;
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|