@grest-ts/websocket 0.0.23 → 0.0.25

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.
Files changed (67) hide show
  1. package/README.md +150 -40
  2. package/dist/src/adapter/NodeSocketAdapter.d.ts +2 -0
  3. package/dist/src/adapter/NodeSocketAdapter.d.ts.map +1 -1
  4. package/dist/src/adapter/NodeSocketAdapter.js +6 -0
  5. package/dist/src/adapter/NodeSocketAdapter.js.map +1 -1
  6. package/dist/src/client/GGSocketPool.d.ts +21 -0
  7. package/dist/src/client/GGSocketPool.d.ts.map +1 -1
  8. package/dist/src/client/GGSocketPool.js +82 -63
  9. package/dist/src/client/GGSocketPool.js.map +1 -1
  10. package/dist/src/client/GGWebSocketSchema.createClient.d.ts +154 -0
  11. package/dist/src/client/GGWebSocketSchema.createClient.d.ts.map +1 -0
  12. package/dist/src/client/GGWebSocketSchema.createClient.js +345 -0
  13. package/dist/src/client/GGWebSocketSchema.createClient.js.map +1 -0
  14. package/dist/src/index-browser.d.ts +2 -1
  15. package/dist/src/index-browser.d.ts.map +1 -1
  16. package/dist/src/index-browser.js +3 -1
  17. package/dist/src/index-browser.js.map +1 -1
  18. package/dist/src/index-node.d.ts +2 -1
  19. package/dist/src/index-node.d.ts.map +1 -1
  20. package/dist/src/index-node.js +2 -1
  21. package/dist/src/index-node.js.map +1 -1
  22. package/dist/src/schema/GGWebSocketMiddleware.d.ts +12 -0
  23. package/dist/src/schema/GGWebSocketMiddleware.d.ts.map +1 -1
  24. package/dist/src/schema/GGWebSocketMiddleware.js +0 -4
  25. package/dist/src/schema/GGWebSocketMiddleware.js.map +1 -1
  26. package/dist/src/schema/GGWebSocketSchema.d.ts +4 -3
  27. package/dist/src/schema/GGWebSocketSchema.d.ts.map +1 -1
  28. package/dist/src/schema/GGWebSocketSchema.js +3 -1
  29. package/dist/src/schema/GGWebSocketSchema.js.map +1 -1
  30. package/dist/src/schema/webSocketSchema.d.ts +12 -6
  31. package/dist/src/schema/webSocketSchema.d.ts.map +1 -1
  32. package/dist/src/schema/webSocketSchema.js +9 -2
  33. package/dist/src/schema/webSocketSchema.js.map +1 -1
  34. package/dist/src/server/GGSocketServer.d.ts.map +1 -1
  35. package/dist/src/server/GGSocketServer.js +36 -2
  36. package/dist/src/server/GGSocketServer.js.map +1 -1
  37. package/dist/src/server/GGWebSocketSchema.startServer.d.ts +5 -3
  38. package/dist/src/server/GGWebSocketSchema.startServer.d.ts.map +1 -1
  39. package/dist/src/server/GGWebSocketSchema.startServer.js +7 -5
  40. package/dist/src/server/GGWebSocketSchema.startServer.js.map +1 -1
  41. package/dist/src/socket/GGSocket.d.ts +13 -1
  42. package/dist/src/socket/GGSocket.d.ts.map +1 -1
  43. package/dist/src/socket/GGSocket.js +52 -2
  44. package/dist/src/socket/GGSocket.js.map +1 -1
  45. package/dist/src/socket/SocketAdapter.d.ts +11 -0
  46. package/dist/src/socket/SocketAdapter.d.ts.map +1 -1
  47. package/dist/testkit/client/GGWebSocketSchema.callOn.d.ts +1 -1
  48. package/dist/testkit/client/GGWebSocketSchema.callOn.d.ts.map +1 -1
  49. package/dist/tsconfig.publish.tsbuildinfo +1 -1
  50. package/package.json +11 -11
  51. package/src/adapter/NodeSocketAdapter.ts +8 -0
  52. package/src/client/GGSocketPool.ts +90 -73
  53. package/src/client/GGWebSocketSchema.createClient.ts +534 -0
  54. package/src/index-browser.ts +5 -2
  55. package/src/index-node.ts +2 -1
  56. package/src/schema/GGWebSocketMiddleware.ts +14 -0
  57. package/src/schema/GGWebSocketSchema.ts +7 -3
  58. package/src/schema/webSocketSchema.ts +18 -8
  59. package/src/server/GGSocketServer.ts +51 -2
  60. package/src/server/GGWebSocketSchema.startServer.ts +14 -10
  61. package/src/socket/GGSocket.ts +56 -2
  62. package/src/socket/SocketAdapter.ts +13 -0
  63. package/dist/src/client/GGSocketClient.d.ts +0 -10
  64. package/dist/src/client/GGSocketClient.d.ts.map +0 -1
  65. package/dist/src/client/GGSocketClient.js +0 -17
  66. package/dist/src/client/GGSocketClient.js.map +0 -1
  67. package/src/client/GGSocketClient.ts +0 -25
@@ -3,7 +3,7 @@ import {GGWebSocketHandshakeContext, GGWebSocketMiddleware} from "../schema/GGWe
3
3
  import {SocketAdapter} from "../socket/SocketAdapter";
4
4
  import {GG_WS_CONNECTION} from "../server/GG_WS_CONNECTION";
5
5
  import {Message, MessageType} from "../socket/SocketMessage";
6
- import {GGValidator, SERVER_ERROR} from "@grest-ts/schema";
6
+ import {GGContractExecutor, GGValidator, SERVER_ERROR} from "@grest-ts/schema";
7
7
  import {withTimeout} from "@grest-ts/common";
8
8
  import {GGContext} from "@grest-ts/context";
9
9
  import {GG_TRACE} from "@grest-ts/trace";
@@ -141,21 +141,13 @@ export class GGSocketPool {
141
141
  static async getOrConnect<Query>(
142
142
  config: GGSocketPoolConfig<Query>
143
143
  ): Promise<GGSocket> {
144
- // Build headers from middlewares
145
144
  const headers = this.buildHeaders(config);
146
-
147
- // Build full URL with query string if provided
148
- let fullUrl = config.domain + config.path;
149
- if (config.query) {
150
- const queryEntries: [string, string][] = Object.entries(config.query).map(([key, value]) => [key, String(value)]);
151
- fullUrl += '?' + new URLSearchParams(queryEntries).toString();
152
- }
145
+ const fullUrl = this.buildUrl(config);
153
146
 
154
147
  // Create connection key based on URL + headers
155
148
  const headerKey = Object.entries(headers).sort().map(([k, v]) => `${k}=${v}`).join('&');
156
149
  const key = fullUrl + "::" + headerKey;
157
150
 
158
- // Check for existing connection first
159
151
  if (this.sockets.has(key)) {
160
152
  return this.sockets.get(key);
161
153
  }
@@ -163,82 +155,107 @@ export class GGSocketPool {
163
155
  return this.pendingSockets.get(key);
164
156
  }
165
157
 
166
- // Create the connection promise BEFORE any async operations to prevent race conditions
167
- // This ensures that concurrent calls will see the pending promise
168
- const connectionPromise = (async () => {
169
- // Ensure adapter is loaded (this is async but safely inside the promise)
170
- const adapterClass = await this.ensureAdapter();
171
-
172
- return new Promise<GGSocket>((resolve, reject) => {
173
- const adapter = new adapterClass(fullUrl);
174
- adapter.onOpen(async () => {
175
- try {
176
- const context = new GGContext("ws-client-connection");
177
- await context.run(async () => {
178
- GG_TRACE.init();
179
- GG_WS_CONNECTION.set({
180
- port: undefined,
181
- path: config.domain
182
- });
183
-
184
- // Send handshake with headers
185
- adapter.send(Message.create(MessageType.HANDSHAKE, "", "", headers));
186
-
187
- // Wait for handshake response
188
- await withTimeout(
189
- new Promise<void>((handshakeResolve, handshakeReject) => {
190
- const onMessage = (data: string) => {
191
- const msg = Message.parse(data);
192
- if (!msg) return;
193
-
194
- if (msg.type === MessageType.HANDSHAKE_OK) {
195
- adapter.offMessage(onMessage);
196
- handshakeResolve();
197
- } else if (msg.type === MessageType.HANDSHAKE_ERR) {
198
- adapter.offMessage(onMessage);
199
- handshakeReject(new SERVER_ERROR({
200
- displayMessage: 'WebSocket handshake failed',
201
- originalError: msg.data
202
- }));
203
- }
204
- };
205
- adapter.onMessage(onMessage);
206
- }),
207
- 5000,
208
- 'Handshake timeout'
209
- );
210
- resolve(new GGSocket(adapter, {connectionContext: context}));
211
- });
212
- } catch (error) {
213
- reject(error);
214
- }
215
- });
216
- adapter.onError((error: Error) => {
217
- reject(error);
218
- });
219
- });
220
- })();
221
-
222
- // Store the pending promise IMMEDIATELY (before awaiting)
158
+ const connectionPromise = this.openSocket(fullUrl, headers, config.domain);
223
159
  this.pendingSockets.set(key, connectionPromise);
224
160
 
225
161
  try {
226
162
  const socket = await connectionPromise;
227
-
228
- // Store the connection
229
163
  this.sockets.set(key, socket);
230
164
  this.pendingSockets.delete(key);
231
-
232
- // Clean up on close
233
165
  socket.onClose(() => {
234
166
  this.sockets.delete(key);
235
167
  });
236
-
237
168
  return socket;
238
169
  } catch (error) {
239
- // Clean up failed connection attempt
240
170
  this.pendingSockets.delete(key);
241
171
  throw error;
242
172
  }
243
173
  }
174
+
175
+ /**
176
+ * Establish a fresh, un-pooled WebSocket connection.
177
+ *
178
+ * Unlike `getOrConnect`, this never reuses or caches connections — every
179
+ * call produces a dedicated socket with its own close lifecycle. Use this
180
+ * when you want each logical client to own its connection (the common
181
+ * case for `createClient()` users).
182
+ */
183
+ static async connect<Query>(
184
+ config: GGSocketPoolConfig<Query>
185
+ ): Promise<GGSocket> {
186
+ return this.openSocket(this.buildUrl(config), this.buildHeaders(config), config.domain);
187
+ }
188
+
189
+ /**
190
+ * Reconstruct the typed error the server threw during handshake.
191
+ *
192
+ * The server sends `error.toJSON()` which has `{success:false, type, data?, context?}`.
193
+ * System errors (NOT_AUTHORIZED, FORBIDDEN, VALIDATION_ERROR, etc.) are reconstructed
194
+ * as real instances so callers can `.toBeError(NOT_AUTHORIZED)`. Anything we can't
195
+ * identify (non-ERROR throw, custom error class the client doesn't know) falls back
196
+ * to SERVER_ERROR carrying the original payload for inspection.
197
+ */
198
+ private static handshakeErrorFrom(payload: any): Error {
199
+ if (payload && typeof payload === 'object' && typeof payload.type === 'string') {
200
+ return GGContractExecutor.createErrorObj(payload) as unknown as Error;
201
+ }
202
+ return new SERVER_ERROR({
203
+ displayMessage: 'WebSocket handshake failed',
204
+ originalError: payload,
205
+ });
206
+ }
207
+
208
+ private static buildUrl(config: GGSocketPoolConfig<any>): string {
209
+ let fullUrl = config.domain + config.path;
210
+ if (config.query) {
211
+ const queryEntries: [string, string][] = Object.entries(config.query).map(([key, value]) => [key, String(value)]);
212
+ fullUrl += '?' + new URLSearchParams(queryEntries).toString();
213
+ }
214
+ return fullUrl;
215
+ }
216
+
217
+ private static async openSocket(fullUrl: string, headers: Record<string, string>, domain: string): Promise<GGSocket> {
218
+ const adapterClass = await this.ensureAdapter();
219
+ return new Promise<GGSocket>((resolve, reject) => {
220
+ const adapter = new adapterClass(fullUrl);
221
+ adapter.onOpen(async () => {
222
+ try {
223
+ const context = new GGContext("ws-client-connection");
224
+ await context.run(async () => {
225
+ GG_TRACE.init();
226
+ GG_WS_CONNECTION.set({
227
+ port: undefined,
228
+ path: domain
229
+ });
230
+ adapter.send(Message.create(MessageType.HANDSHAKE, "", "", headers));
231
+ await withTimeout(
232
+ new Promise<void>((handshakeResolve, handshakeReject) => {
233
+ const onMessage = (data: string) => {
234
+ const msg = Message.parse(data);
235
+ if (!msg) return;
236
+
237
+ if (msg.type === MessageType.HANDSHAKE_OK) {
238
+ adapter.offMessage(onMessage);
239
+ handshakeResolve();
240
+ } else if (msg.type === MessageType.HANDSHAKE_ERR) {
241
+ adapter.offMessage(onMessage);
242
+ handshakeReject(this.handshakeErrorFrom(msg.data));
243
+ }
244
+ };
245
+ adapter.onMessage(onMessage);
246
+ }),
247
+ 5000,
248
+ 'Handshake timeout'
249
+ );
250
+ resolve(new GGSocket(adapter, {connectionContext: context}));
251
+ });
252
+ } catch (error) {
253
+ reject(error);
254
+ }
255
+ });
256
+ adapter.onError((error: Error) => {
257
+ reject(error);
258
+ });
259
+ });
260
+ }
244
261
  }