nitro-graphql 1.6.1 → 1.7.0-beta.0
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/dist/ecosystem/nuxt.mjs +3 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +23 -1
- package/dist/rollup.mjs +2 -2
- package/dist/routes/apollo-server-ws.d.mts +6 -0
- package/dist/routes/apollo-server-ws.mjs +298 -0
- package/dist/routes/apollo-server.d.mts +2 -2
- package/dist/routes/apollo-server.mjs +2 -2
- package/dist/routes/debug.d.mts +2 -2
- package/dist/routes/graphql-yoga-ws.d.mts +6 -0
- package/dist/routes/graphql-yoga-ws.mjs +298 -0
- package/dist/routes/graphql-yoga.d.mts +2 -2
- package/dist/subscribe/index.d.mts +146 -0
- package/dist/subscribe/index.mjs +830 -0
- package/dist/templates/subscribe-client.mjs +59 -0
- package/dist/types/index.d.mts +16 -1
- package/dist/utils/apollo.d.mts +1 -1
- package/dist/utils/apollo.mjs +1 -1
- package/dist/utils/client-codegen.d.mts +15 -1
- package/dist/utils/client-codegen.mjs +407 -8
- package/dist/utils/define.d.mts +1 -1
- package/dist/utils/type-generation.mjs +7 -3
- package/dist/utils/ws-protocol.d.mts +25 -0
- package/dist/utils/ws-protocol.mjs +99 -0
- package/dist/utils/ws-schema.d.mts +6 -0
- package/dist/utils/ws-schema.mjs +58 -0
- package/package.json +14 -9
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
//#region src/subscribe/index.ts
|
|
2
|
+
const DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
|
|
3
|
+
const DEFAULT_MAX_RETRIES = 5;
|
|
4
|
+
const MAX_BACKOFF_MS = 3e4;
|
|
5
|
+
const DEFAULT_PING_INTERVAL_MS = 25e3;
|
|
6
|
+
const DEFAULT_PONG_TIMEOUT_MS = 5e3;
|
|
7
|
+
function generateSubscriptionId() {
|
|
8
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
9
|
+
}
|
|
10
|
+
function calculateBackoffDelay(retryCount) {
|
|
11
|
+
const baseDelay = 1e3 * 2 ** retryCount;
|
|
12
|
+
return Math.min(baseDelay, MAX_BACKOFF_MS) + Math.random() * 1e3;
|
|
13
|
+
}
|
|
14
|
+
function extractErrorMessage(errors, fallback) {
|
|
15
|
+
return errors?.[0]?.message || fallback;
|
|
16
|
+
}
|
|
17
|
+
function extractDataValue(data) {
|
|
18
|
+
if (!data) return;
|
|
19
|
+
return Object.values(data)[0];
|
|
20
|
+
}
|
|
21
|
+
function isWebSocketAvailable() {
|
|
22
|
+
return typeof globalThis.WebSocket !== "undefined";
|
|
23
|
+
}
|
|
24
|
+
function toWebSocketUrl(httpUrl) {
|
|
25
|
+
if (httpUrl.startsWith("/")) return `${typeof window !== "undefined" && window.location.protocol === "https:" ? "wss:" : "ws:"}//${typeof window !== "undefined" ? window.location.host : "localhost"}${httpUrl}`;
|
|
26
|
+
return httpUrl.replace(/^http/, "ws");
|
|
27
|
+
}
|
|
28
|
+
function parseMessage(data) {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(data);
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function createTimerManager() {
|
|
36
|
+
const manager = {
|
|
37
|
+
reconnectTimeout: null,
|
|
38
|
+
connectionTimeout: null,
|
|
39
|
+
pingInterval: null,
|
|
40
|
+
pongTimeout: null,
|
|
41
|
+
clearReconnect() {
|
|
42
|
+
if (manager.reconnectTimeout) {
|
|
43
|
+
clearTimeout(manager.reconnectTimeout);
|
|
44
|
+
manager.reconnectTimeout = null;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
clearConnection() {
|
|
48
|
+
if (manager.connectionTimeout) {
|
|
49
|
+
clearTimeout(manager.connectionTimeout);
|
|
50
|
+
manager.connectionTimeout = null;
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
clearPing() {
|
|
54
|
+
if (manager.pingInterval) {
|
|
55
|
+
clearInterval(manager.pingInterval);
|
|
56
|
+
manager.pingInterval = null;
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
clearPong() {
|
|
60
|
+
if (manager.pongTimeout) {
|
|
61
|
+
clearTimeout(manager.pongTimeout);
|
|
62
|
+
manager.pongTimeout = null;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
clearAll() {
|
|
66
|
+
manager.clearReconnect();
|
|
67
|
+
manager.clearConnection();
|
|
68
|
+
manager.clearPing();
|
|
69
|
+
manager.clearPong();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
return manager;
|
|
73
|
+
}
|
|
74
|
+
function createKeepAliveHandler(config, timers) {
|
|
75
|
+
function start() {
|
|
76
|
+
stop();
|
|
77
|
+
timers.pingInterval = setInterval(() => {
|
|
78
|
+
if (!config.isActive()) return;
|
|
79
|
+
config.sendMessage({ type: "ping" });
|
|
80
|
+
timers.pongTimeout = setTimeout(() => {
|
|
81
|
+
config.onPongTimeout();
|
|
82
|
+
}, DEFAULT_PONG_TIMEOUT_MS);
|
|
83
|
+
}, DEFAULT_PING_INTERVAL_MS);
|
|
84
|
+
}
|
|
85
|
+
function stop() {
|
|
86
|
+
timers.clearPing();
|
|
87
|
+
timers.clearPong();
|
|
88
|
+
}
|
|
89
|
+
function handlePong() {
|
|
90
|
+
timers.clearPong();
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
start,
|
|
94
|
+
stop,
|
|
95
|
+
handlePong
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function createDedicatedSubscription(endpoint, options) {
|
|
99
|
+
let ws = null;
|
|
100
|
+
let isConnected = false;
|
|
101
|
+
let connectionState = "idle";
|
|
102
|
+
let retryCount = 0;
|
|
103
|
+
let subscriptionId = null;
|
|
104
|
+
let intentionalClose = false;
|
|
105
|
+
let hasConnectedBefore = false;
|
|
106
|
+
let messageIdCounter = 0;
|
|
107
|
+
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
108
|
+
const connectionTimeoutMs = options.connectionTimeoutMs ?? DEFAULT_CONNECTION_TIMEOUT_MS;
|
|
109
|
+
const id = generateSubscriptionId();
|
|
110
|
+
const timers = createTimerManager();
|
|
111
|
+
function setState(state) {
|
|
112
|
+
connectionState = state;
|
|
113
|
+
options.onStateChange?.(state);
|
|
114
|
+
}
|
|
115
|
+
function sendMessage(message) {
|
|
116
|
+
if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(message));
|
|
117
|
+
}
|
|
118
|
+
const keepAlive = createKeepAliveHandler({
|
|
119
|
+
sendMessage,
|
|
120
|
+
isActive: () => isConnected,
|
|
121
|
+
onPongTimeout: () => {
|
|
122
|
+
options.onError?.(/* @__PURE__ */ new Error("Server not responding to ping"));
|
|
123
|
+
cleanupConnection();
|
|
124
|
+
scheduleReconnect();
|
|
125
|
+
}
|
|
126
|
+
}, timers);
|
|
127
|
+
function subscribe() {
|
|
128
|
+
subscriptionId = String(++messageIdCounter);
|
|
129
|
+
sendMessage({
|
|
130
|
+
id: subscriptionId,
|
|
131
|
+
type: "subscribe",
|
|
132
|
+
payload: {
|
|
133
|
+
query: options.query,
|
|
134
|
+
variables: options.variables
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function cleanupConnection() {
|
|
139
|
+
keepAlive.stop();
|
|
140
|
+
if (ws) {
|
|
141
|
+
if (subscriptionId) sendMessage({
|
|
142
|
+
id: subscriptionId,
|
|
143
|
+
type: "complete"
|
|
144
|
+
});
|
|
145
|
+
ws.close();
|
|
146
|
+
ws = null;
|
|
147
|
+
isConnected = false;
|
|
148
|
+
subscriptionId = null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function scheduleReconnect() {
|
|
152
|
+
timers.clearReconnect();
|
|
153
|
+
if (retryCount < maxRetries) {
|
|
154
|
+
retryCount++;
|
|
155
|
+
setState("reconnecting");
|
|
156
|
+
options.onRetrying?.(retryCount, maxRetries);
|
|
157
|
+
const delay = calculateBackoffDelay(retryCount);
|
|
158
|
+
timers.reconnectTimeout = setTimeout(connect, delay);
|
|
159
|
+
} else {
|
|
160
|
+
setState("error");
|
|
161
|
+
options.onMaxRetriesReached?.();
|
|
162
|
+
options.onError?.(/* @__PURE__ */ new Error("Max reconnection attempts reached"));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function handleMessage(data) {
|
|
166
|
+
const message = parseMessage(data);
|
|
167
|
+
if (!message) {
|
|
168
|
+
options.onError?.(/* @__PURE__ */ new Error("Failed to parse message"));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
switch (message.type) {
|
|
172
|
+
case "connection_ack":
|
|
173
|
+
handleConnectionAck();
|
|
174
|
+
break;
|
|
175
|
+
case "connection_error":
|
|
176
|
+
handleConnectionError(message);
|
|
177
|
+
break;
|
|
178
|
+
case "next":
|
|
179
|
+
handleNext(message);
|
|
180
|
+
break;
|
|
181
|
+
case "error":
|
|
182
|
+
handleSubscriptionError(message);
|
|
183
|
+
break;
|
|
184
|
+
case "complete":
|
|
185
|
+
subscriptionId = null;
|
|
186
|
+
break;
|
|
187
|
+
case "ping":
|
|
188
|
+
sendMessage({ type: "pong" });
|
|
189
|
+
break;
|
|
190
|
+
case "pong":
|
|
191
|
+
keepAlive.handlePong();
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function handleConnectionAck() {
|
|
196
|
+
timers.clearConnection();
|
|
197
|
+
isConnected = true;
|
|
198
|
+
retryCount = 0;
|
|
199
|
+
setState("connected");
|
|
200
|
+
if (hasConnectedBefore) options.onReconnected?.();
|
|
201
|
+
else {
|
|
202
|
+
hasConnectedBefore = true;
|
|
203
|
+
options.onConnected?.();
|
|
204
|
+
}
|
|
205
|
+
keepAlive.start();
|
|
206
|
+
subscribe();
|
|
207
|
+
}
|
|
208
|
+
function handleConnectionError(message) {
|
|
209
|
+
timers.clearConnection();
|
|
210
|
+
const payload = message.payload;
|
|
211
|
+
options.onError?.(new Error(payload?.message || "Connection rejected"));
|
|
212
|
+
setState("error");
|
|
213
|
+
}
|
|
214
|
+
function handleNext(message) {
|
|
215
|
+
if (message.payload && typeof message.payload === "object") {
|
|
216
|
+
const payload = message.payload;
|
|
217
|
+
if (payload.errors) options.onError?.(new Error(extractErrorMessage(payload.errors, "GraphQL Error")));
|
|
218
|
+
else if (payload.data) {
|
|
219
|
+
const value = extractDataValue(payload.data);
|
|
220
|
+
if (value !== void 0) options.onData?.(value);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function handleSubscriptionError(message) {
|
|
225
|
+
if (Array.isArray(message.payload)) {
|
|
226
|
+
const errors = message.payload;
|
|
227
|
+
options.onError?.(new Error(extractErrorMessage(errors, "Subscription error")));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function connect() {
|
|
231
|
+
if (!isWebSocketAvailable()) {
|
|
232
|
+
options.onError?.(/* @__PURE__ */ new Error("WebSocket not available"));
|
|
233
|
+
setState("error");
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
timers.clearReconnect();
|
|
237
|
+
cleanupConnection();
|
|
238
|
+
setState("connecting");
|
|
239
|
+
const wsUrl = toWebSocketUrl(endpoint);
|
|
240
|
+
ws = new WebSocket(wsUrl, "graphql-transport-ws");
|
|
241
|
+
ws.onopen = () => {
|
|
242
|
+
timers.connectionTimeout = setTimeout(() => {
|
|
243
|
+
options.onError?.(/* @__PURE__ */ new Error("Connection timeout"));
|
|
244
|
+
setState("error");
|
|
245
|
+
cleanupConnection();
|
|
246
|
+
scheduleReconnect();
|
|
247
|
+
}, connectionTimeoutMs);
|
|
248
|
+
sendMessage({
|
|
249
|
+
type: "connection_init",
|
|
250
|
+
payload: options.connectionParams || {}
|
|
251
|
+
});
|
|
252
|
+
};
|
|
253
|
+
ws.onmessage = (event) => {
|
|
254
|
+
handleMessage(typeof event.data === "string" ? event.data : String(event.data));
|
|
255
|
+
};
|
|
256
|
+
ws.onerror = () => {
|
|
257
|
+
options.onError?.(/* @__PURE__ */ new Error("WebSocket connection error"));
|
|
258
|
+
};
|
|
259
|
+
ws.onclose = () => {
|
|
260
|
+
isConnected = false;
|
|
261
|
+
subscriptionId = null;
|
|
262
|
+
timers.clearConnection();
|
|
263
|
+
options.onDisconnected?.();
|
|
264
|
+
if (intentionalClose) {
|
|
265
|
+
intentionalClose = false;
|
|
266
|
+
setState("disconnected");
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
scheduleReconnect();
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function disconnect() {
|
|
273
|
+
intentionalClose = true;
|
|
274
|
+
timers.clearAll();
|
|
275
|
+
cleanupConnection();
|
|
276
|
+
setState("disconnected");
|
|
277
|
+
}
|
|
278
|
+
connect();
|
|
279
|
+
return {
|
|
280
|
+
unsubscribe: disconnect,
|
|
281
|
+
get isConnected() {
|
|
282
|
+
return isConnected;
|
|
283
|
+
},
|
|
284
|
+
get state() {
|
|
285
|
+
return connectionState;
|
|
286
|
+
},
|
|
287
|
+
id,
|
|
288
|
+
transport: "websocket"
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function createSession(endpoint, connectionParams, maxRetries, connectionTimeoutMs) {
|
|
292
|
+
let ws = null;
|
|
293
|
+
let connectionState = "idle";
|
|
294
|
+
let retryCount = 0;
|
|
295
|
+
let hasConnectedBefore = false;
|
|
296
|
+
let intentionalClose = false;
|
|
297
|
+
const subscriptions = /* @__PURE__ */ new Map();
|
|
298
|
+
const pendingSubscriptions = [];
|
|
299
|
+
let messageIdCounter = 0;
|
|
300
|
+
const stateChangeListeners = /* @__PURE__ */ new Set();
|
|
301
|
+
const timers = createTimerManager();
|
|
302
|
+
function notifyStateChange() {
|
|
303
|
+
for (const listener of stateChangeListeners) listener(connectionState, subscriptions.size);
|
|
304
|
+
}
|
|
305
|
+
function setState(state) {
|
|
306
|
+
connectionState = state;
|
|
307
|
+
for (const sub of subscriptions.values()) sub.onStateChange?.(state);
|
|
308
|
+
notifyStateChange();
|
|
309
|
+
}
|
|
310
|
+
function generateId() {
|
|
311
|
+
return String(++messageIdCounter);
|
|
312
|
+
}
|
|
313
|
+
function sendMessage(message) {
|
|
314
|
+
if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(message));
|
|
315
|
+
}
|
|
316
|
+
const keepAlive = createKeepAliveHandler({
|
|
317
|
+
sendMessage,
|
|
318
|
+
isActive: () => connectionState === "connected",
|
|
319
|
+
onPongTimeout: () => {
|
|
320
|
+
notifyAllError(/* @__PURE__ */ new Error("Server not responding to ping"));
|
|
321
|
+
cleanupConnection();
|
|
322
|
+
scheduleReconnect();
|
|
323
|
+
}
|
|
324
|
+
}, timers);
|
|
325
|
+
function notifyAllError(error) {
|
|
326
|
+
for (const sub of subscriptions.values()) sub.onError?.(error);
|
|
327
|
+
}
|
|
328
|
+
function cleanupConnection() {
|
|
329
|
+
keepAlive.stop();
|
|
330
|
+
if (ws) {
|
|
331
|
+
ws.close();
|
|
332
|
+
ws = null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function scheduleReconnect() {
|
|
336
|
+
timers.clearReconnect();
|
|
337
|
+
if (retryCount < maxRetries) {
|
|
338
|
+
retryCount++;
|
|
339
|
+
setState("reconnecting");
|
|
340
|
+
for (const sub of subscriptions.values()) sub.onRetrying?.(retryCount, maxRetries);
|
|
341
|
+
const delay = calculateBackoffDelay(retryCount);
|
|
342
|
+
timers.reconnectTimeout = setTimeout(() => connect(), delay);
|
|
343
|
+
} else {
|
|
344
|
+
setState("error");
|
|
345
|
+
for (const sub of subscriptions.values()) {
|
|
346
|
+
sub.onMaxRetriesReached?.();
|
|
347
|
+
sub.onError?.(/* @__PURE__ */ new Error("Max reconnection attempts reached"));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function sendSubscribe(sub) {
|
|
352
|
+
sendMessage({
|
|
353
|
+
id: sub.id,
|
|
354
|
+
type: "subscribe",
|
|
355
|
+
payload: {
|
|
356
|
+
query: sub.query,
|
|
357
|
+
variables: sub.variables
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
function subscribeInternal(sub) {
|
|
362
|
+
subscriptions.set(sub.id, sub);
|
|
363
|
+
notifyStateChange();
|
|
364
|
+
if (connectionState === "connected") {
|
|
365
|
+
sendSubscribe(sub);
|
|
366
|
+
if (!sub.hasNotifiedConnect) {
|
|
367
|
+
sub.hasNotifiedConnect = true;
|
|
368
|
+
sub.onConnected?.();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function handleMessage(data) {
|
|
373
|
+
const message = parseMessage(data);
|
|
374
|
+
if (!message) {
|
|
375
|
+
notifyAllError(/* @__PURE__ */ new Error("Failed to parse message"));
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
switch (message.type) {
|
|
379
|
+
case "connection_ack":
|
|
380
|
+
handleConnectionAck();
|
|
381
|
+
break;
|
|
382
|
+
case "connection_error":
|
|
383
|
+
handleConnectionError(message);
|
|
384
|
+
break;
|
|
385
|
+
case "next":
|
|
386
|
+
handleNext(message);
|
|
387
|
+
break;
|
|
388
|
+
case "error":
|
|
389
|
+
handleSubscriptionError(message);
|
|
390
|
+
break;
|
|
391
|
+
case "complete":
|
|
392
|
+
handleComplete(message);
|
|
393
|
+
break;
|
|
394
|
+
case "ping":
|
|
395
|
+
sendMessage({ type: "pong" });
|
|
396
|
+
break;
|
|
397
|
+
case "pong":
|
|
398
|
+
keepAlive.handlePong();
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function handleConnectionAck() {
|
|
403
|
+
timers.clearConnection();
|
|
404
|
+
retryCount = 0;
|
|
405
|
+
setState("connected");
|
|
406
|
+
keepAlive.start();
|
|
407
|
+
const isReconnect = hasConnectedBefore;
|
|
408
|
+
hasConnectedBefore = true;
|
|
409
|
+
for (const sub of pendingSubscriptions) subscribeInternal(sub);
|
|
410
|
+
pendingSubscriptions.length = 0;
|
|
411
|
+
if (isReconnect) for (const sub of subscriptions.values()) {
|
|
412
|
+
sub.onReconnected?.();
|
|
413
|
+
sendSubscribe(sub);
|
|
414
|
+
}
|
|
415
|
+
else for (const sub of subscriptions.values()) if (!sub.hasNotifiedConnect) {
|
|
416
|
+
sub.hasNotifiedConnect = true;
|
|
417
|
+
sub.onConnected?.();
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function handleConnectionError(message) {
|
|
421
|
+
timers.clearConnection();
|
|
422
|
+
const payload = message.payload;
|
|
423
|
+
notifyAllError(new Error(payload?.message || "Connection rejected"));
|
|
424
|
+
setState("error");
|
|
425
|
+
}
|
|
426
|
+
function handleNext(message) {
|
|
427
|
+
if (!message.id) return;
|
|
428
|
+
const sub = subscriptions.get(message.id);
|
|
429
|
+
if (!sub) return;
|
|
430
|
+
if (message.payload && typeof message.payload === "object") {
|
|
431
|
+
const payload = message.payload;
|
|
432
|
+
if (payload.errors) sub.onError?.(new Error(extractErrorMessage(payload.errors, "GraphQL Error")));
|
|
433
|
+
else if (payload.data) {
|
|
434
|
+
const value = extractDataValue(payload.data);
|
|
435
|
+
if (value !== void 0) sub.onData?.(value);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function handleSubscriptionError(message) {
|
|
440
|
+
if (!message.id) return;
|
|
441
|
+
const sub = subscriptions.get(message.id);
|
|
442
|
+
if (sub && Array.isArray(message.payload)) {
|
|
443
|
+
const errors = message.payload;
|
|
444
|
+
sub.onError?.(new Error(extractErrorMessage(errors, "Subscription error")));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function handleComplete(message) {
|
|
448
|
+
if (message.id) {
|
|
449
|
+
subscriptions.delete(message.id);
|
|
450
|
+
notifyStateChange();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function connect() {
|
|
454
|
+
if (connectionState === "connected" || connectionState === "connecting") return;
|
|
455
|
+
if (!isWebSocketAvailable()) {
|
|
456
|
+
notifyAllError(/* @__PURE__ */ new Error("WebSocket not available"));
|
|
457
|
+
setState("error");
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
timers.clearReconnect();
|
|
461
|
+
setState("connecting");
|
|
462
|
+
const wsUrl = toWebSocketUrl(endpoint);
|
|
463
|
+
ws = new WebSocket(wsUrl, "graphql-transport-ws");
|
|
464
|
+
ws.onopen = () => {
|
|
465
|
+
timers.connectionTimeout = setTimeout(() => {
|
|
466
|
+
notifyAllError(/* @__PURE__ */ new Error("Connection timeout"));
|
|
467
|
+
setState("error");
|
|
468
|
+
cleanupConnection();
|
|
469
|
+
scheduleReconnect();
|
|
470
|
+
}, connectionTimeoutMs);
|
|
471
|
+
sendMessage({
|
|
472
|
+
type: "connection_init",
|
|
473
|
+
payload: connectionParams
|
|
474
|
+
});
|
|
475
|
+
};
|
|
476
|
+
ws.onmessage = (event) => {
|
|
477
|
+
handleMessage(typeof event.data === "string" ? event.data : String(event.data));
|
|
478
|
+
};
|
|
479
|
+
ws.onerror = () => {
|
|
480
|
+
notifyAllError(/* @__PURE__ */ new Error("WebSocket connection error"));
|
|
481
|
+
};
|
|
482
|
+
ws.onclose = () => {
|
|
483
|
+
timers.clearConnection();
|
|
484
|
+
for (const sub of subscriptions.values()) sub.onDisconnected?.();
|
|
485
|
+
if (intentionalClose) {
|
|
486
|
+
intentionalClose = false;
|
|
487
|
+
setState("disconnected");
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
scheduleReconnect();
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function unsubscribe(id) {
|
|
494
|
+
if (subscriptions.get(id)) {
|
|
495
|
+
if (connectionState === "connected") sendMessage({
|
|
496
|
+
id,
|
|
497
|
+
type: "complete"
|
|
498
|
+
});
|
|
499
|
+
subscriptions.delete(id);
|
|
500
|
+
notifyStateChange();
|
|
501
|
+
}
|
|
502
|
+
const pendingIdx = pendingSubscriptions.findIndex((s) => s.id === id);
|
|
503
|
+
if (pendingIdx !== -1) pendingSubscriptions.splice(pendingIdx, 1);
|
|
504
|
+
if (subscriptions.size === 0 && pendingSubscriptions.length === 0) close();
|
|
505
|
+
}
|
|
506
|
+
function close() {
|
|
507
|
+
intentionalClose = true;
|
|
508
|
+
timers.clearAll();
|
|
509
|
+
if (connectionState === "connected") for (const sub of subscriptions.values()) sendMessage({
|
|
510
|
+
id: sub.id,
|
|
511
|
+
type: "complete"
|
|
512
|
+
});
|
|
513
|
+
subscriptions.clear();
|
|
514
|
+
pendingSubscriptions.length = 0;
|
|
515
|
+
cleanupConnection();
|
|
516
|
+
setState("disconnected");
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
subscribe(query, variables, onData, onError) {
|
|
520
|
+
const id = generateId();
|
|
521
|
+
const entry = {
|
|
522
|
+
id,
|
|
523
|
+
query,
|
|
524
|
+
variables,
|
|
525
|
+
onData,
|
|
526
|
+
onError,
|
|
527
|
+
hasNotifiedConnect: false
|
|
528
|
+
};
|
|
529
|
+
if (connectionState === "connected") subscribeInternal(entry);
|
|
530
|
+
else {
|
|
531
|
+
pendingSubscriptions.push(entry);
|
|
532
|
+
notifyStateChange();
|
|
533
|
+
connect();
|
|
534
|
+
}
|
|
535
|
+
return {
|
|
536
|
+
unsubscribe: () => unsubscribe(id),
|
|
537
|
+
get isConnected() {
|
|
538
|
+
return connectionState === "connected";
|
|
539
|
+
},
|
|
540
|
+
get state() {
|
|
541
|
+
return connectionState;
|
|
542
|
+
},
|
|
543
|
+
id,
|
|
544
|
+
transport: "websocket"
|
|
545
|
+
};
|
|
546
|
+
},
|
|
547
|
+
get state() {
|
|
548
|
+
return connectionState;
|
|
549
|
+
},
|
|
550
|
+
get isConnected() {
|
|
551
|
+
return connectionState === "connected";
|
|
552
|
+
},
|
|
553
|
+
get subscriptionCount() {
|
|
554
|
+
return subscriptions.size;
|
|
555
|
+
},
|
|
556
|
+
close,
|
|
557
|
+
onStateChange(callback) {
|
|
558
|
+
stateChangeListeners.add(callback);
|
|
559
|
+
return () => stateChangeListeners.delete(callback);
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Resolve transport type from options
|
|
565
|
+
*/
|
|
566
|
+
function resolveTransport(options) {
|
|
567
|
+
if (options?.sse) return "sse";
|
|
568
|
+
return options?.transport ?? "websocket";
|
|
569
|
+
}
|
|
570
|
+
function createSubscriptionClient(config = {}) {
|
|
571
|
+
const wsEndpoint = config.wsEndpoint ?? "/api/graphql/ws";
|
|
572
|
+
const sseEndpoint = config.sseEndpoint ?? "/api/graphql";
|
|
573
|
+
const configConnectionParams = config.connectionParams;
|
|
574
|
+
const connectionTimeoutMs = config.connectionTimeoutMs ?? DEFAULT_CONNECTION_TIMEOUT_MS;
|
|
575
|
+
const maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
576
|
+
function getConnectionParams() {
|
|
577
|
+
if (!configConnectionParams) return {};
|
|
578
|
+
if (typeof configConnectionParams === "function") {
|
|
579
|
+
const result = configConnectionParams();
|
|
580
|
+
if (result instanceof Promise) {
|
|
581
|
+
console.warn("[nitro-graphql] Async connectionParams not supported in subscribe(), use subscribeAsync()");
|
|
582
|
+
return {};
|
|
583
|
+
}
|
|
584
|
+
return result;
|
|
585
|
+
}
|
|
586
|
+
return configConnectionParams;
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
subscribe(query, variables, onData, onError, transportOptions) {
|
|
590
|
+
const transport = resolveTransport(transportOptions);
|
|
591
|
+
const subscriptionOptions = {
|
|
592
|
+
query,
|
|
593
|
+
variables,
|
|
594
|
+
onData,
|
|
595
|
+
onError,
|
|
596
|
+
connectionParams: getConnectionParams(),
|
|
597
|
+
connectionTimeoutMs,
|
|
598
|
+
maxRetries
|
|
599
|
+
};
|
|
600
|
+
if (transport === "sse") return createSseDedicatedSubscription(sseEndpoint, subscriptionOptions);
|
|
601
|
+
if (transport === "auto") return createAutoSubscription(wsEndpoint, sseEndpoint, subscriptionOptions);
|
|
602
|
+
return createDedicatedSubscription(wsEndpoint, subscriptionOptions);
|
|
603
|
+
},
|
|
604
|
+
async subscribeAsync(options, transportOptions) {
|
|
605
|
+
let connectionParams = options.connectionParams;
|
|
606
|
+
if (!connectionParams && configConnectionParams) connectionParams = typeof configConnectionParams === "function" ? await configConnectionParams() : configConnectionParams;
|
|
607
|
+
const transport = resolveTransport(transportOptions);
|
|
608
|
+
const subscriptionOptions = {
|
|
609
|
+
...options,
|
|
610
|
+
connectionParams,
|
|
611
|
+
connectionTimeoutMs: options.connectionTimeoutMs ?? connectionTimeoutMs,
|
|
612
|
+
maxRetries: options.maxRetries ?? maxRetries
|
|
613
|
+
};
|
|
614
|
+
if (transport === "sse") return createSseDedicatedSubscription(sseEndpoint, subscriptionOptions);
|
|
615
|
+
if (transport === "auto") return createAutoSubscription(wsEndpoint, sseEndpoint, subscriptionOptions);
|
|
616
|
+
return createDedicatedSubscription(wsEndpoint, subscriptionOptions);
|
|
617
|
+
},
|
|
618
|
+
createSession() {
|
|
619
|
+
return createSession(wsEndpoint, getConnectionParams(), maxRetries, connectionTimeoutMs);
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Create an SSE subscription using native EventSource (legacy API)
|
|
625
|
+
* Use this when WebSocket is blocked (corporate firewalls, etc.)
|
|
626
|
+
*
|
|
627
|
+
* @deprecated Use the unified client with { transport: 'sse' } instead
|
|
628
|
+
*
|
|
629
|
+
* @example
|
|
630
|
+
* ```typescript
|
|
631
|
+
* const handle = createSseSubscription('/api/graphql', {
|
|
632
|
+
* query: 'subscription { countdown(from: 10) }',
|
|
633
|
+
* onData: (data) => console.log(data),
|
|
634
|
+
* })
|
|
635
|
+
*
|
|
636
|
+
* // Later...
|
|
637
|
+
* handle.close()
|
|
638
|
+
* ```
|
|
639
|
+
*/
|
|
640
|
+
function createSseSubscription(endpoint, options) {
|
|
641
|
+
const url = new URL(endpoint, typeof window !== "undefined" ? window.location.origin : "http://localhost");
|
|
642
|
+
url.searchParams.set("query", options.query);
|
|
643
|
+
if (options.variables) url.searchParams.set("variables", JSON.stringify(options.variables));
|
|
644
|
+
const es = new EventSource(url.toString());
|
|
645
|
+
es.onmessage = (event) => {
|
|
646
|
+
try {
|
|
647
|
+
const response = JSON.parse(event.data);
|
|
648
|
+
if (response.errors && response.errors.length > 0) options.onError?.(new Error(response.errors[0]?.message || "SSE Error"));
|
|
649
|
+
else if (response.data) {
|
|
650
|
+
const value = Object.values(response.data)[0];
|
|
651
|
+
options.onData?.(value);
|
|
652
|
+
}
|
|
653
|
+
} catch {}
|
|
654
|
+
};
|
|
655
|
+
es.onerror = () => {
|
|
656
|
+
options.onError?.(/* @__PURE__ */ new Error("SSE connection error"));
|
|
657
|
+
es.close();
|
|
658
|
+
};
|
|
659
|
+
return { close: () => es.close() };
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Create an SSE subscription with full state management (matching WebSocket API)
|
|
663
|
+
* Uses native EventSource which handles reconnection automatically.
|
|
664
|
+
*/
|
|
665
|
+
function createSseDedicatedSubscription(endpoint, options) {
|
|
666
|
+
let es = null;
|
|
667
|
+
let connectionState = "idle";
|
|
668
|
+
let hasConnectedBefore = false;
|
|
669
|
+
let intentionalClose = false;
|
|
670
|
+
const id = generateSubscriptionId();
|
|
671
|
+
function setState(state) {
|
|
672
|
+
connectionState = state;
|
|
673
|
+
options.onStateChange?.(state);
|
|
674
|
+
}
|
|
675
|
+
function buildUrl() {
|
|
676
|
+
const url = new URL(endpoint, typeof window !== "undefined" ? window.location.origin : "http://localhost");
|
|
677
|
+
url.searchParams.set("query", options.query);
|
|
678
|
+
if (options.variables) url.searchParams.set("variables", JSON.stringify(options.variables));
|
|
679
|
+
return url.toString();
|
|
680
|
+
}
|
|
681
|
+
function connect() {
|
|
682
|
+
if (typeof EventSource === "undefined") {
|
|
683
|
+
options.onError?.(/* @__PURE__ */ new Error("EventSource not available"));
|
|
684
|
+
setState("error");
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
setState("connecting");
|
|
688
|
+
es = new EventSource(buildUrl());
|
|
689
|
+
es.onopen = () => {
|
|
690
|
+
setState("connected");
|
|
691
|
+
if (hasConnectedBefore) options.onReconnected?.();
|
|
692
|
+
else {
|
|
693
|
+
hasConnectedBefore = true;
|
|
694
|
+
options.onConnected?.();
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
function handleSseData(event) {
|
|
698
|
+
try {
|
|
699
|
+
const response = JSON.parse(event.data);
|
|
700
|
+
if (response.errors && response.errors.length > 0) options.onError?.(new Error(response.errors[0]?.message || "SSE Error"));
|
|
701
|
+
else if (response.data) {
|
|
702
|
+
const value = extractDataValue(response.data);
|
|
703
|
+
if (value !== void 0) options.onData?.(value);
|
|
704
|
+
}
|
|
705
|
+
} catch {}
|
|
706
|
+
}
|
|
707
|
+
es.addEventListener("next", handleSseData);
|
|
708
|
+
es.onmessage = handleSseData;
|
|
709
|
+
es.onerror = () => {
|
|
710
|
+
if (es?.readyState === EventSource.CONNECTING) {
|
|
711
|
+
setState("reconnecting");
|
|
712
|
+
options.onRetrying?.(1, 999);
|
|
713
|
+
} else if (es?.readyState === EventSource.CLOSED) {
|
|
714
|
+
if (!intentionalClose) {
|
|
715
|
+
options.onError?.(/* @__PURE__ */ new Error("SSE connection closed"));
|
|
716
|
+
setState("error");
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
function disconnect() {
|
|
722
|
+
intentionalClose = true;
|
|
723
|
+
if (es) {
|
|
724
|
+
es.close();
|
|
725
|
+
es = null;
|
|
726
|
+
}
|
|
727
|
+
options.onDisconnected?.();
|
|
728
|
+
setState("disconnected");
|
|
729
|
+
}
|
|
730
|
+
connect();
|
|
731
|
+
return {
|
|
732
|
+
unsubscribe: disconnect,
|
|
733
|
+
get isConnected() {
|
|
734
|
+
return connectionState === "connected";
|
|
735
|
+
},
|
|
736
|
+
get state() {
|
|
737
|
+
return connectionState;
|
|
738
|
+
},
|
|
739
|
+
id,
|
|
740
|
+
transport: "sse"
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Create a subscription with auto transport selection
|
|
745
|
+
* Tries WebSocket first, falls back to SSE if WebSocket fails to connect
|
|
746
|
+
*/
|
|
747
|
+
function createAutoSubscription(wsEndpoint, sseEndpoint, options) {
|
|
748
|
+
let activeHandle = null;
|
|
749
|
+
let activeTransport = "websocket";
|
|
750
|
+
let connectionState = "idle";
|
|
751
|
+
const id = generateSubscriptionId();
|
|
752
|
+
let hasNotifiedConnect = false;
|
|
753
|
+
const wrappedOptions = {
|
|
754
|
+
...options,
|
|
755
|
+
onStateChange: (state) => {
|
|
756
|
+
connectionState = state;
|
|
757
|
+
options.onStateChange?.(state);
|
|
758
|
+
},
|
|
759
|
+
onConnected: () => {
|
|
760
|
+
if (!hasNotifiedConnect) {
|
|
761
|
+
hasNotifiedConnect = true;
|
|
762
|
+
options.onConnected?.();
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
|
+
onReconnected: () => {
|
|
766
|
+
options.onReconnected?.();
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
function tryWebSocket() {
|
|
770
|
+
if (!isWebSocketAvailable()) {
|
|
771
|
+
trySSE();
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
activeTransport = "websocket";
|
|
775
|
+
let wsConnectionTimeout = null;
|
|
776
|
+
let wsConnected = false;
|
|
777
|
+
activeHandle = createDedicatedSubscription(wsEndpoint, {
|
|
778
|
+
...wrappedOptions,
|
|
779
|
+
onConnected: () => {
|
|
780
|
+
wsConnected = true;
|
|
781
|
+
if (wsConnectionTimeout) {
|
|
782
|
+
clearTimeout(wsConnectionTimeout);
|
|
783
|
+
wsConnectionTimeout = null;
|
|
784
|
+
}
|
|
785
|
+
wrappedOptions.onConnected?.();
|
|
786
|
+
},
|
|
787
|
+
onError: (error) => {
|
|
788
|
+
if (!wsConnected && !hasNotifiedConnect) {
|
|
789
|
+
activeHandle?.unsubscribe();
|
|
790
|
+
trySSE();
|
|
791
|
+
} else options.onError?.(error);
|
|
792
|
+
},
|
|
793
|
+
onMaxRetriesReached: () => {
|
|
794
|
+
if (!wsConnected && !hasNotifiedConnect) {
|
|
795
|
+
activeHandle?.unsubscribe();
|
|
796
|
+
trySSE();
|
|
797
|
+
} else options.onMaxRetriesReached?.();
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
wsConnectionTimeout = setTimeout(() => {
|
|
801
|
+
if (!wsConnected && !hasNotifiedConnect) {
|
|
802
|
+
activeHandle?.unsubscribe();
|
|
803
|
+
trySSE();
|
|
804
|
+
}
|
|
805
|
+
}, options.connectionTimeoutMs ?? DEFAULT_CONNECTION_TIMEOUT_MS);
|
|
806
|
+
}
|
|
807
|
+
function trySSE() {
|
|
808
|
+
activeTransport = "sse";
|
|
809
|
+
activeHandle = createSseDedicatedSubscription(sseEndpoint, wrappedOptions);
|
|
810
|
+
}
|
|
811
|
+
tryWebSocket();
|
|
812
|
+
return {
|
|
813
|
+
unsubscribe: () => {
|
|
814
|
+
activeHandle?.unsubscribe();
|
|
815
|
+
},
|
|
816
|
+
get isConnected() {
|
|
817
|
+
return activeHandle?.isConnected ?? false;
|
|
818
|
+
},
|
|
819
|
+
get state() {
|
|
820
|
+
return connectionState;
|
|
821
|
+
},
|
|
822
|
+
id,
|
|
823
|
+
get transport() {
|
|
824
|
+
return activeTransport;
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
//#endregion
|
|
830
|
+
export { createSseSubscription, createSubscriptionClient };
|