@trpc/client 10.43.0 → 11.0.0-next.91
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 -2
- package/dist/TRPCClientError.d.ts +6 -8
- package/dist/TRPCClientError.d.ts.map +1 -1
- package/dist/bundle-analysis.json +352 -0
- package/dist/createTRPCClient.d.ts +41 -14
- package/dist/createTRPCClient.d.ts.map +1 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -10
- package/dist/index.mjs +2 -10
- package/dist/links/HTTPBatchLinkOptions.d.ts +0 -6
- package/dist/links/HTTPBatchLinkOptions.d.ts.map +1 -1
- package/dist/links/wsLink.d.ts +16 -5
- package/dist/links/wsLink.d.ts.map +1 -1
- package/dist/links/wsLink.js +132 -117
- package/dist/links/wsLink.mjs +132 -117
- package/package.json +4 -4
- package/src/TRPCClientError.ts +7 -21
- package/src/createTRPCClient.ts +137 -45
- package/src/index.ts +15 -1
- package/src/links/HTTPBatchLinkOptions.ts +0 -5
- package/src/links/wsLink.ts +183 -140
- package/dist/createTRPCClientProxy.d.ts +0 -49
- package/dist/createTRPCClientProxy.d.ts.map +0 -1
- package/links/formDataLink/index.d.ts +0 -1
- package/links/formDataLink/index.js +0 -1
- package/links/httpTuplesonLink/index.d.ts +0 -1
- package/links/httpTuplesonLink/index.js +0 -1
- package/src/createTRPCClientProxy.ts +0 -146
package/dist/links/wsLink.js
CHANGED
|
@@ -8,6 +8,7 @@ var TRPCClientError = require('../TRPCClientError-e224e397.js');
|
|
|
8
8
|
|
|
9
9
|
/* istanbul ignore next -- @preserve */ const retryDelay = (attemptIndex)=>attemptIndex === 0 ? 0 : Math.min(1000 * 2 ** attemptIndex, 30000);
|
|
10
10
|
|
|
11
|
+
const run = (fn)=>fn();
|
|
11
12
|
function createWSClient(opts) {
|
|
12
13
|
const { url , WebSocket: WebSocketImpl = WebSocket , retryDelayMs: retryDelayFn = retryDelay , onOpen , onClose , } = opts;
|
|
13
14
|
/* istanbul ignore next -- @preserve */ if (!WebSocketImpl) {
|
|
@@ -18,163 +19,171 @@ function createWSClient(opts) {
|
|
|
18
19
|
*/ let outgoing = [];
|
|
19
20
|
const pendingRequests = Object.create(null);
|
|
20
21
|
let connectAttempt = 0;
|
|
21
|
-
let
|
|
22
|
-
let
|
|
23
|
-
let activeConnection =
|
|
24
|
-
|
|
22
|
+
let connectTimer = undefined;
|
|
23
|
+
let connectionIndex = 0;
|
|
24
|
+
let activeConnection = createConnection();
|
|
25
|
+
/**
|
|
26
|
+
* Global connection has been killed
|
|
27
|
+
*/ let killed = false;
|
|
25
28
|
/**
|
|
26
29
|
* tries to send the list of messages
|
|
27
30
|
*/ function dispatch() {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
// using a timeout to batch messages
|
|
32
|
+
setTimeout(()=>{
|
|
33
|
+
if (activeConnection?.state !== 'open') {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
for (const pending of Object.values(pendingRequests)){
|
|
37
|
+
if (!pending.connection) {
|
|
38
|
+
pending.connection = activeConnection;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
33
41
|
if (outgoing.length === 1) {
|
|
34
42
|
// single send
|
|
35
|
-
activeConnection.send(JSON.stringify(outgoing.pop()));
|
|
43
|
+
activeConnection.ws.send(JSON.stringify(outgoing.pop()));
|
|
36
44
|
} else {
|
|
37
45
|
// batch send
|
|
38
|
-
activeConnection.send(JSON.stringify(outgoing));
|
|
46
|
+
activeConnection.ws.send(JSON.stringify(outgoing));
|
|
39
47
|
}
|
|
40
48
|
// clear
|
|
41
49
|
outgoing = [];
|
|
42
50
|
});
|
|
43
51
|
}
|
|
44
52
|
function tryReconnect() {
|
|
45
|
-
if (connectTimer
|
|
53
|
+
if (!!connectTimer || killed) {
|
|
46
54
|
return;
|
|
47
55
|
}
|
|
48
56
|
const timeout = retryDelayFn(connectAttempt++);
|
|
49
57
|
reconnectInMs(timeout);
|
|
50
58
|
}
|
|
51
59
|
function reconnect() {
|
|
52
|
-
state = 'connecting';
|
|
53
60
|
const oldConnection = activeConnection;
|
|
54
|
-
activeConnection =
|
|
55
|
-
closeIfNoPending(oldConnection);
|
|
61
|
+
activeConnection = createConnection();
|
|
62
|
+
oldConnection && closeIfNoPending(oldConnection);
|
|
56
63
|
}
|
|
57
64
|
function reconnectInMs(ms) {
|
|
58
65
|
if (connectTimer) {
|
|
59
66
|
return;
|
|
60
67
|
}
|
|
61
|
-
state = 'connecting';
|
|
62
68
|
connectTimer = setTimeout(reconnect, ms);
|
|
63
69
|
}
|
|
64
70
|
function closeIfNoPending(conn) {
|
|
65
71
|
// disconnect as soon as there are are no pending result
|
|
66
|
-
const hasPendingRequests = Object.values(pendingRequests).some((p)=>p.
|
|
72
|
+
const hasPendingRequests = Object.values(pendingRequests).some((p)=>p.connection === conn);
|
|
67
73
|
if (!hasPendingRequests) {
|
|
68
|
-
conn.close();
|
|
74
|
+
conn.ws?.close();
|
|
69
75
|
}
|
|
70
76
|
}
|
|
71
|
-
function closeActiveSubscriptions() {
|
|
72
|
-
Object.values(pendingRequests).forEach((req)=>{
|
|
73
|
-
if (req.type === 'subscription') {
|
|
74
|
-
req.callbacks.complete();
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
77
|
function resumeSubscriptionOnReconnect(req) {
|
|
79
78
|
if (outgoing.some((r)=>r.id === req.op.id)) {
|
|
80
79
|
return;
|
|
81
80
|
}
|
|
82
81
|
request(req.op, req.callbacks);
|
|
83
82
|
}
|
|
84
|
-
function
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
connectAttempt = 0;
|
|
94
|
-
state = 'open';
|
|
95
|
-
onOpen?.();
|
|
96
|
-
dispatch();
|
|
97
|
-
});
|
|
98
|
-
conn.addEventListener('error', ()=>{
|
|
99
|
-
if (conn === activeConnection) {
|
|
83
|
+
function createConnection() {
|
|
84
|
+
const self = {
|
|
85
|
+
id: ++connectionIndex,
|
|
86
|
+
state: 'connecting'
|
|
87
|
+
};
|
|
88
|
+
const onError = ()=>{
|
|
89
|
+
self.state = 'closed';
|
|
90
|
+
if (self === activeConnection) {
|
|
100
91
|
tryReconnect();
|
|
101
92
|
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
93
|
+
};
|
|
94
|
+
run(async ()=>{
|
|
95
|
+
const urlString = typeof url === 'function' ? await url() : url;
|
|
96
|
+
const ws = new WebSocketImpl(urlString);
|
|
97
|
+
self.ws = ws;
|
|
98
|
+
clearTimeout(connectTimer);
|
|
99
|
+
connectTimer = undefined;
|
|
100
|
+
ws.addEventListener('open', ()=>{
|
|
101
|
+
/* istanbul ignore next -- @preserve */ if (activeConnection?.ws !== ws) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
connectAttempt = 0;
|
|
105
|
+
self.state = 'open';
|
|
106
|
+
onOpen?.();
|
|
107
|
+
dispatch();
|
|
108
|
+
});
|
|
109
|
+
ws.addEventListener('error', onError);
|
|
110
|
+
const handleIncomingRequest = (req)=>{
|
|
111
|
+
if (self !== activeConnection) {
|
|
112
|
+
return;
|
|
107
113
|
}
|
|
108
|
-
reconnect
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
114
|
+
if (req.method === 'reconnect') {
|
|
115
|
+
reconnect();
|
|
116
|
+
// notify subscribers
|
|
117
|
+
for (const pendingReq of Object.values(pendingRequests)){
|
|
118
|
+
if (pendingReq.type === 'subscription') {
|
|
119
|
+
resumeSubscriptionOnReconnect(pendingReq);
|
|
120
|
+
}
|
|
113
121
|
}
|
|
114
122
|
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
req.callbacks.next?.(data);
|
|
124
|
-
if (req.ws !== activeConnection && conn === activeConnection) {
|
|
125
|
-
const oldWs = req.ws;
|
|
126
|
-
// gracefully replace old connection with this
|
|
127
|
-
req.ws = activeConnection;
|
|
128
|
-
closeIfNoPending(oldWs);
|
|
129
|
-
}
|
|
130
|
-
if ('result' in data && data.result.type === 'stopped' && conn === activeConnection) {
|
|
131
|
-
req.callbacks.complete();
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
conn.addEventListener('message', ({ data })=>{
|
|
135
|
-
const msg = JSON.parse(data);
|
|
136
|
-
if ('method' in msg) {
|
|
137
|
-
handleIncomingRequest(msg);
|
|
138
|
-
} else {
|
|
139
|
-
handleIncomingResponse(msg);
|
|
140
|
-
}
|
|
141
|
-
if (conn !== activeConnection || state === 'closed') {
|
|
142
|
-
// when receiving a message, we close old connection that has no pending requests
|
|
143
|
-
closeIfNoPending(conn);
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
conn.addEventListener('close', ({ code })=>{
|
|
147
|
-
if (state === 'open') {
|
|
148
|
-
onClose?.({
|
|
149
|
-
code
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
if (activeConnection === conn) {
|
|
153
|
-
// connection might have been replaced already
|
|
154
|
-
tryReconnect();
|
|
155
|
-
}
|
|
156
|
-
for (const [key, req] of Object.entries(pendingRequests)){
|
|
157
|
-
if (req.ws !== conn) {
|
|
158
|
-
continue;
|
|
123
|
+
};
|
|
124
|
+
const handleIncomingResponse = (data)=>{
|
|
125
|
+
const req = data.id !== null && pendingRequests[data.id];
|
|
126
|
+
if (!req) {
|
|
127
|
+
// do something?
|
|
128
|
+
return;
|
|
159
129
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
req.
|
|
164
|
-
|
|
130
|
+
req.callbacks.next?.(data);
|
|
131
|
+
if (self === activeConnection && req.connection !== activeConnection) {
|
|
132
|
+
// gracefully replace old connection with this
|
|
133
|
+
const oldConn = req.connection;
|
|
134
|
+
req.connection = self;
|
|
135
|
+
oldConn && closeIfNoPending(oldConn);
|
|
165
136
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
137
|
+
if ('result' in data && data.result.type === 'stopped' && activeConnection === self) {
|
|
138
|
+
req.callbacks.complete();
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
ws.addEventListener('message', ({ data })=>{
|
|
142
|
+
const msg = JSON.parse(data);
|
|
143
|
+
if ('method' in msg) {
|
|
144
|
+
handleIncomingRequest(msg);
|
|
170
145
|
} else {
|
|
171
|
-
|
|
172
|
-
delete pendingRequests[key];
|
|
173
|
-
req.callbacks.error?.(TRPCClientError.TRPCClientError.from(new TRPCWebSocketClosedError('WebSocket closed prematurely')));
|
|
146
|
+
handleIncomingResponse(msg);
|
|
174
147
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
148
|
+
if (self !== activeConnection) {
|
|
149
|
+
// when receiving a message, we close old connection that has no pending requests
|
|
150
|
+
closeIfNoPending(self);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
ws.addEventListener('close', ({ code })=>{
|
|
154
|
+
if (self.state === 'open') {
|
|
155
|
+
onClose?.({
|
|
156
|
+
code
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
self.state = 'closed';
|
|
160
|
+
if (activeConnection === self) {
|
|
161
|
+
// connection might have been replaced already
|
|
162
|
+
tryReconnect();
|
|
163
|
+
}
|
|
164
|
+
for (const [key, req] of Object.entries(pendingRequests)){
|
|
165
|
+
if (req.connection !== self) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (self.state === 'closed') {
|
|
169
|
+
// If the connection was closed, we just call `complete()` on the request
|
|
170
|
+
delete pendingRequests[key];
|
|
171
|
+
req.callbacks.complete?.();
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
// The connection was closed either unexpectedly or because of a reconnect
|
|
175
|
+
if (req.type === 'subscription') {
|
|
176
|
+
// Subscriptions will resume after we've reconnected
|
|
177
|
+
resumeSubscriptionOnReconnect(req);
|
|
178
|
+
} else {
|
|
179
|
+
// Queries and mutations will error if interrupted
|
|
180
|
+
delete pendingRequests[key];
|
|
181
|
+
req.callbacks.error?.(TRPCClientError.TRPCClientError.from(new TRPCWebSocketClosedError('WebSocket closed prematurely')));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}).catch(onError);
|
|
186
|
+
return self;
|
|
178
187
|
}
|
|
179
188
|
function request(op, callbacks) {
|
|
180
189
|
const { type , input , path , id } = op;
|
|
@@ -187,7 +196,7 @@ function createWSClient(opts) {
|
|
|
187
196
|
}
|
|
188
197
|
};
|
|
189
198
|
pendingRequests[id] = {
|
|
190
|
-
|
|
199
|
+
connection: null,
|
|
191
200
|
type,
|
|
192
201
|
callbacks,
|
|
193
202
|
op
|
|
@@ -200,7 +209,7 @@ function createWSClient(opts) {
|
|
|
200
209
|
delete pendingRequests[id];
|
|
201
210
|
outgoing = outgoing.filter((msg)=>msg.id !== id);
|
|
202
211
|
callbacks?.complete?.();
|
|
203
|
-
if (activeConnection
|
|
212
|
+
if (activeConnection?.state === 'open' && op.type === 'subscription') {
|
|
204
213
|
outgoing.push({
|
|
205
214
|
id,
|
|
206
215
|
method: 'subscription.stop'
|
|
@@ -211,12 +220,18 @@ function createWSClient(opts) {
|
|
|
211
220
|
}
|
|
212
221
|
return {
|
|
213
222
|
close: ()=>{
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
223
|
+
killed = true;
|
|
224
|
+
for (const req of Object.values(pendingRequests)){
|
|
225
|
+
if (req.type === 'subscription') {
|
|
226
|
+
req.callbacks.complete();
|
|
227
|
+
} else if (!req.connection) {
|
|
228
|
+
// close pending requests that aren't attached to a connection yet
|
|
229
|
+
req.callbacks.error(TRPCClientError.TRPCClientError.from(new Error('Closed before connection was established')));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
activeConnection && closeIfNoPending(activeConnection);
|
|
218
233
|
clearTimeout(connectTimer);
|
|
219
|
-
connectTimer =
|
|
234
|
+
connectTimer = undefined;
|
|
220
235
|
},
|
|
221
236
|
request,
|
|
222
237
|
getConnection () {
|
package/dist/links/wsLink.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { T as TRPCClientError } from '../TRPCClientError-0de4d231.mjs';
|
|
|
4
4
|
|
|
5
5
|
/* istanbul ignore next -- @preserve */ const retryDelay = (attemptIndex)=>attemptIndex === 0 ? 0 : Math.min(1000 * 2 ** attemptIndex, 30000);
|
|
6
6
|
|
|
7
|
+
const run = (fn)=>fn();
|
|
7
8
|
function createWSClient(opts) {
|
|
8
9
|
const { url , WebSocket: WebSocketImpl = WebSocket , retryDelayMs: retryDelayFn = retryDelay , onOpen , onClose , } = opts;
|
|
9
10
|
/* istanbul ignore next -- @preserve */ if (!WebSocketImpl) {
|
|
@@ -14,163 +15,171 @@ function createWSClient(opts) {
|
|
|
14
15
|
*/ let outgoing = [];
|
|
15
16
|
const pendingRequests = Object.create(null);
|
|
16
17
|
let connectAttempt = 0;
|
|
17
|
-
let
|
|
18
|
-
let
|
|
19
|
-
let activeConnection =
|
|
20
|
-
|
|
18
|
+
let connectTimer = undefined;
|
|
19
|
+
let connectionIndex = 0;
|
|
20
|
+
let activeConnection = createConnection();
|
|
21
|
+
/**
|
|
22
|
+
* Global connection has been killed
|
|
23
|
+
*/ let killed = false;
|
|
21
24
|
/**
|
|
22
25
|
* tries to send the list of messages
|
|
23
26
|
*/ function dispatch() {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
// using a timeout to batch messages
|
|
28
|
+
setTimeout(()=>{
|
|
29
|
+
if (activeConnection?.state !== 'open') {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
for (const pending of Object.values(pendingRequests)){
|
|
33
|
+
if (!pending.connection) {
|
|
34
|
+
pending.connection = activeConnection;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
29
37
|
if (outgoing.length === 1) {
|
|
30
38
|
// single send
|
|
31
|
-
activeConnection.send(JSON.stringify(outgoing.pop()));
|
|
39
|
+
activeConnection.ws.send(JSON.stringify(outgoing.pop()));
|
|
32
40
|
} else {
|
|
33
41
|
// batch send
|
|
34
|
-
activeConnection.send(JSON.stringify(outgoing));
|
|
42
|
+
activeConnection.ws.send(JSON.stringify(outgoing));
|
|
35
43
|
}
|
|
36
44
|
// clear
|
|
37
45
|
outgoing = [];
|
|
38
46
|
});
|
|
39
47
|
}
|
|
40
48
|
function tryReconnect() {
|
|
41
|
-
if (connectTimer
|
|
49
|
+
if (!!connectTimer || killed) {
|
|
42
50
|
return;
|
|
43
51
|
}
|
|
44
52
|
const timeout = retryDelayFn(connectAttempt++);
|
|
45
53
|
reconnectInMs(timeout);
|
|
46
54
|
}
|
|
47
55
|
function reconnect() {
|
|
48
|
-
state = 'connecting';
|
|
49
56
|
const oldConnection = activeConnection;
|
|
50
|
-
activeConnection =
|
|
51
|
-
closeIfNoPending(oldConnection);
|
|
57
|
+
activeConnection = createConnection();
|
|
58
|
+
oldConnection && closeIfNoPending(oldConnection);
|
|
52
59
|
}
|
|
53
60
|
function reconnectInMs(ms) {
|
|
54
61
|
if (connectTimer) {
|
|
55
62
|
return;
|
|
56
63
|
}
|
|
57
|
-
state = 'connecting';
|
|
58
64
|
connectTimer = setTimeout(reconnect, ms);
|
|
59
65
|
}
|
|
60
66
|
function closeIfNoPending(conn) {
|
|
61
67
|
// disconnect as soon as there are are no pending result
|
|
62
|
-
const hasPendingRequests = Object.values(pendingRequests).some((p)=>p.
|
|
68
|
+
const hasPendingRequests = Object.values(pendingRequests).some((p)=>p.connection === conn);
|
|
63
69
|
if (!hasPendingRequests) {
|
|
64
|
-
conn.close();
|
|
70
|
+
conn.ws?.close();
|
|
65
71
|
}
|
|
66
72
|
}
|
|
67
|
-
function closeActiveSubscriptions() {
|
|
68
|
-
Object.values(pendingRequests).forEach((req)=>{
|
|
69
|
-
if (req.type === 'subscription') {
|
|
70
|
-
req.callbacks.complete();
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
73
|
function resumeSubscriptionOnReconnect(req) {
|
|
75
74
|
if (outgoing.some((r)=>r.id === req.op.id)) {
|
|
76
75
|
return;
|
|
77
76
|
}
|
|
78
77
|
request(req.op, req.callbacks);
|
|
79
78
|
}
|
|
80
|
-
function
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
connectAttempt = 0;
|
|
90
|
-
state = 'open';
|
|
91
|
-
onOpen?.();
|
|
92
|
-
dispatch();
|
|
93
|
-
});
|
|
94
|
-
conn.addEventListener('error', ()=>{
|
|
95
|
-
if (conn === activeConnection) {
|
|
79
|
+
function createConnection() {
|
|
80
|
+
const self = {
|
|
81
|
+
id: ++connectionIndex,
|
|
82
|
+
state: 'connecting'
|
|
83
|
+
};
|
|
84
|
+
const onError = ()=>{
|
|
85
|
+
self.state = 'closed';
|
|
86
|
+
if (self === activeConnection) {
|
|
96
87
|
tryReconnect();
|
|
97
88
|
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
89
|
+
};
|
|
90
|
+
run(async ()=>{
|
|
91
|
+
const urlString = typeof url === 'function' ? await url() : url;
|
|
92
|
+
const ws = new WebSocketImpl(urlString);
|
|
93
|
+
self.ws = ws;
|
|
94
|
+
clearTimeout(connectTimer);
|
|
95
|
+
connectTimer = undefined;
|
|
96
|
+
ws.addEventListener('open', ()=>{
|
|
97
|
+
/* istanbul ignore next -- @preserve */ if (activeConnection?.ws !== ws) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
connectAttempt = 0;
|
|
101
|
+
self.state = 'open';
|
|
102
|
+
onOpen?.();
|
|
103
|
+
dispatch();
|
|
104
|
+
});
|
|
105
|
+
ws.addEventListener('error', onError);
|
|
106
|
+
const handleIncomingRequest = (req)=>{
|
|
107
|
+
if (self !== activeConnection) {
|
|
108
|
+
return;
|
|
103
109
|
}
|
|
104
|
-
reconnect
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
110
|
+
if (req.method === 'reconnect') {
|
|
111
|
+
reconnect();
|
|
112
|
+
// notify subscribers
|
|
113
|
+
for (const pendingReq of Object.values(pendingRequests)){
|
|
114
|
+
if (pendingReq.type === 'subscription') {
|
|
115
|
+
resumeSubscriptionOnReconnect(pendingReq);
|
|
116
|
+
}
|
|
109
117
|
}
|
|
110
118
|
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
req.callbacks.next?.(data);
|
|
120
|
-
if (req.ws !== activeConnection && conn === activeConnection) {
|
|
121
|
-
const oldWs = req.ws;
|
|
122
|
-
// gracefully replace old connection with this
|
|
123
|
-
req.ws = activeConnection;
|
|
124
|
-
closeIfNoPending(oldWs);
|
|
125
|
-
}
|
|
126
|
-
if ('result' in data && data.result.type === 'stopped' && conn === activeConnection) {
|
|
127
|
-
req.callbacks.complete();
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
conn.addEventListener('message', ({ data })=>{
|
|
131
|
-
const msg = JSON.parse(data);
|
|
132
|
-
if ('method' in msg) {
|
|
133
|
-
handleIncomingRequest(msg);
|
|
134
|
-
} else {
|
|
135
|
-
handleIncomingResponse(msg);
|
|
136
|
-
}
|
|
137
|
-
if (conn !== activeConnection || state === 'closed') {
|
|
138
|
-
// when receiving a message, we close old connection that has no pending requests
|
|
139
|
-
closeIfNoPending(conn);
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
conn.addEventListener('close', ({ code })=>{
|
|
143
|
-
if (state === 'open') {
|
|
144
|
-
onClose?.({
|
|
145
|
-
code
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
if (activeConnection === conn) {
|
|
149
|
-
// connection might have been replaced already
|
|
150
|
-
tryReconnect();
|
|
151
|
-
}
|
|
152
|
-
for (const [key, req] of Object.entries(pendingRequests)){
|
|
153
|
-
if (req.ws !== conn) {
|
|
154
|
-
continue;
|
|
119
|
+
};
|
|
120
|
+
const handleIncomingResponse = (data)=>{
|
|
121
|
+
const req = data.id !== null && pendingRequests[data.id];
|
|
122
|
+
if (!req) {
|
|
123
|
+
// do something?
|
|
124
|
+
return;
|
|
155
125
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
req.
|
|
160
|
-
|
|
126
|
+
req.callbacks.next?.(data);
|
|
127
|
+
if (self === activeConnection && req.connection !== activeConnection) {
|
|
128
|
+
// gracefully replace old connection with this
|
|
129
|
+
const oldConn = req.connection;
|
|
130
|
+
req.connection = self;
|
|
131
|
+
oldConn && closeIfNoPending(oldConn);
|
|
161
132
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
133
|
+
if ('result' in data && data.result.type === 'stopped' && activeConnection === self) {
|
|
134
|
+
req.callbacks.complete();
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
ws.addEventListener('message', ({ data })=>{
|
|
138
|
+
const msg = JSON.parse(data);
|
|
139
|
+
if ('method' in msg) {
|
|
140
|
+
handleIncomingRequest(msg);
|
|
166
141
|
} else {
|
|
167
|
-
|
|
168
|
-
delete pendingRequests[key];
|
|
169
|
-
req.callbacks.error?.(TRPCClientError.from(new TRPCWebSocketClosedError('WebSocket closed prematurely')));
|
|
142
|
+
handleIncomingResponse(msg);
|
|
170
143
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
144
|
+
if (self !== activeConnection) {
|
|
145
|
+
// when receiving a message, we close old connection that has no pending requests
|
|
146
|
+
closeIfNoPending(self);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
ws.addEventListener('close', ({ code })=>{
|
|
150
|
+
if (self.state === 'open') {
|
|
151
|
+
onClose?.({
|
|
152
|
+
code
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
self.state = 'closed';
|
|
156
|
+
if (activeConnection === self) {
|
|
157
|
+
// connection might have been replaced already
|
|
158
|
+
tryReconnect();
|
|
159
|
+
}
|
|
160
|
+
for (const [key, req] of Object.entries(pendingRequests)){
|
|
161
|
+
if (req.connection !== self) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (self.state === 'closed') {
|
|
165
|
+
// If the connection was closed, we just call `complete()` on the request
|
|
166
|
+
delete pendingRequests[key];
|
|
167
|
+
req.callbacks.complete?.();
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
// The connection was closed either unexpectedly or because of a reconnect
|
|
171
|
+
if (req.type === 'subscription') {
|
|
172
|
+
// Subscriptions will resume after we've reconnected
|
|
173
|
+
resumeSubscriptionOnReconnect(req);
|
|
174
|
+
} else {
|
|
175
|
+
// Queries and mutations will error if interrupted
|
|
176
|
+
delete pendingRequests[key];
|
|
177
|
+
req.callbacks.error?.(TRPCClientError.from(new TRPCWebSocketClosedError('WebSocket closed prematurely')));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}).catch(onError);
|
|
182
|
+
return self;
|
|
174
183
|
}
|
|
175
184
|
function request(op, callbacks) {
|
|
176
185
|
const { type , input , path , id } = op;
|
|
@@ -183,7 +192,7 @@ function createWSClient(opts) {
|
|
|
183
192
|
}
|
|
184
193
|
};
|
|
185
194
|
pendingRequests[id] = {
|
|
186
|
-
|
|
195
|
+
connection: null,
|
|
187
196
|
type,
|
|
188
197
|
callbacks,
|
|
189
198
|
op
|
|
@@ -196,7 +205,7 @@ function createWSClient(opts) {
|
|
|
196
205
|
delete pendingRequests[id];
|
|
197
206
|
outgoing = outgoing.filter((msg)=>msg.id !== id);
|
|
198
207
|
callbacks?.complete?.();
|
|
199
|
-
if (activeConnection
|
|
208
|
+
if (activeConnection?.state === 'open' && op.type === 'subscription') {
|
|
200
209
|
outgoing.push({
|
|
201
210
|
id,
|
|
202
211
|
method: 'subscription.stop'
|
|
@@ -207,12 +216,18 @@ function createWSClient(opts) {
|
|
|
207
216
|
}
|
|
208
217
|
return {
|
|
209
218
|
close: ()=>{
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
219
|
+
killed = true;
|
|
220
|
+
for (const req of Object.values(pendingRequests)){
|
|
221
|
+
if (req.type === 'subscription') {
|
|
222
|
+
req.callbacks.complete();
|
|
223
|
+
} else if (!req.connection) {
|
|
224
|
+
// close pending requests that aren't attached to a connection yet
|
|
225
|
+
req.callbacks.error(TRPCClientError.from(new Error('Closed before connection was established')));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
activeConnection && closeIfNoPending(activeConnection);
|
|
214
229
|
clearTimeout(connectTimer);
|
|
215
|
-
connectTimer =
|
|
230
|
+
connectTimer = undefined;
|
|
216
231
|
},
|
|
217
232
|
request,
|
|
218
233
|
getConnection () {
|