@rebasepro/client 0.3.0 → 0.5.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/src/websocket.ts CHANGED
@@ -46,6 +46,8 @@ export interface RebaseWebSocketConfig {
46
46
  getAuthToken?: () => Promise<string>;
47
47
  /** Optional WebSocket constructor to override globalThis.WebSocket (e.g. for Node environments) */
48
48
  WebSocket?: typeof WebSocket;
49
+ /** Callback to handle unauthorized requests or token expiration (refreshes auth session) */
50
+ onUnauthorized?: () => Promise<boolean>;
49
51
  }
50
52
 
51
53
 
@@ -131,10 +133,13 @@ export class RebaseWebSocketClient {
131
133
  private isAuthenticated = false;
132
134
  private authPromise: Promise<void> | null = null;
133
135
  private WebSocketConstructor: typeof WebSocket | undefined;
136
+ public onUnauthorized?: () => Promise<boolean>;
137
+ private refreshInProgress: Promise<boolean> | null = null;
134
138
 
135
139
  constructor(config: RebaseWebSocketConfig) {
136
140
  this.websocketUrl = config.websocketUrl;
137
141
  this.getAuthToken = config.getAuthToken;
142
+ this.onUnauthorized = config.onUnauthorized;
138
143
  this.WebSocketConstructor = config.WebSocket || (typeof WebSocket !== "undefined" ? WebSocket : undefined);
139
144
 
140
145
  if (!this.WebSocketConstructor) {
@@ -335,6 +340,44 @@ export class RebaseWebSocketClient {
335
340
  }, delay);
336
341
  }
337
342
 
343
+ private isAuthError(message: WebSocketMessage): boolean {
344
+ if (message.type === "AUTH_ERROR") return true;
345
+ const { errorMessage, errorCode } = extractMessageError(message);
346
+ if (errorCode === "UNAUTHORIZED" || errorCode === "JWT_EXPIRED" || errorCode === "AUTH_ERROR") return true;
347
+ const lowerMessage = errorMessage.toLowerCase();
348
+ return lowerMessage.includes("unauthorized") || lowerMessage.includes("token expired") || lowerMessage.includes("token is expired") || lowerMessage.includes("invalid token") || lowerMessage.includes("session expired") || lowerMessage.includes("auth error");
349
+ }
350
+
351
+ private async handleAuthFailure(): Promise<boolean> {
352
+ if (this.refreshInProgress) {
353
+ return this.refreshInProgress;
354
+ }
355
+ this.refreshInProgress = (async () => {
356
+ this.isAuthenticated = false;
357
+ this.authPromise = null;
358
+ if (this.onUnauthorized) {
359
+ try {
360
+ const refreshed = await this.onUnauthorized();
361
+ if (refreshed && this.getAuthToken) {
362
+ const token = await this.getAuthToken();
363
+ if (token) {
364
+ await this.authenticate(token);
365
+ return true;
366
+ }
367
+ }
368
+ } catch (error) {
369
+ console.error("WebSocket auth refresh failed:", error);
370
+ }
371
+ }
372
+ return false;
373
+ })();
374
+ try {
375
+ return await this.refreshInProgress;
376
+ } finally {
377
+ this.refreshInProgress = null;
378
+ }
379
+ }
380
+
338
381
  private handleWebSocketMessage(message: WebSocketMessage) {
339
382
  const {
340
383
  type,
@@ -344,17 +387,28 @@ export class RebaseWebSocketClient {
344
387
 
345
388
  // Handle responses to pending requests
346
389
  if (requestId && this.pendingRequests.has(requestId)) {
347
- const {
348
- resolve,
349
- reject
350
- } = this.pendingRequests.get(requestId)!;
351
- this.pendingRequests.delete(requestId);
352
-
390
+ const pendingReq = this.pendingRequests.get(requestId)!;
353
391
  if (type === "ERROR" || type === "AUTH_ERROR" || message.error) {
354
- const { errorMessage, errorCode } = extractMessageError(message);
355
- reject(new ApiError(errorMessage, errorMessage, errorCode));
392
+ if (this.isAuthError(message)) {
393
+ this.pendingRequests.delete(requestId);
394
+ this.handleAuthFailure().then(refreshed => {
395
+ if (refreshed && pendingReq.message) {
396
+ this.doSendMessage(pendingReq.message, pendingReq.resolve, pendingReq.reject).catch(pendingReq.reject);
397
+ } else {
398
+ const { errorMessage, errorCode } = extractMessageError(message);
399
+ pendingReq.reject(new ApiError(errorMessage, errorMessage, errorCode));
400
+ }
401
+ }).catch(err => {
402
+ pendingReq.reject(err);
403
+ });
404
+ } else {
405
+ this.pendingRequests.delete(requestId);
406
+ const { errorMessage, errorCode } = extractMessageError(message);
407
+ pendingReq.reject(new ApiError(errorMessage, errorMessage, errorCode));
408
+ }
356
409
  } else {
357
- resolve(message.payload || message);
410
+ this.pendingRequests.delete(requestId);
411
+ pendingReq.resolve(message.payload || message);
358
412
  }
359
413
  return;
360
414
  }
@@ -474,6 +528,42 @@ export class RebaseWebSocketClient {
474
528
  if (collectionKey) {
475
529
  const collectionSub = this.collectionSubscriptions.get(collectionKey);
476
530
  if (collectionSub) {
531
+ if (this.isAuthError(message)) {
532
+ this.handleAuthFailure().then(refreshed => {
533
+ if (refreshed) {
534
+ const oldBackendId = collectionSub.backendSubscriptionId;
535
+ const newBackendId = `collection_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
536
+ collectionSub.backendSubscriptionId = newBackendId;
537
+ this.backendToCollectionKey.delete(oldBackendId);
538
+ this.backendToCollectionKey.set(newBackendId, collectionKey);
539
+
540
+ this.sendMessage({
541
+ type: "subscribe_collection",
542
+ payload: {
543
+ ...collectionSub.props,
544
+ subscriptionId: newBackendId
545
+ }
546
+ }).catch(error => {
547
+ console.error("[WS] Failed to re-subscribe collection after auth refresh:", collectionKey, error);
548
+ collectionSub.callbacks.forEach(callback => {
549
+ if (callback.onError) callback.onError(error);
550
+ });
551
+ });
552
+ } else {
553
+ const { errorMessage, errorCode } = extractMessageError(message);
554
+ const error = new ApiError(errorMessage, errorMessage, errorCode);
555
+ collectionSub.callbacks.forEach(callback => {
556
+ if (callback.onError) callback.onError(error);
557
+ });
558
+ }
559
+ }).catch(err => {
560
+ collectionSub.callbacks.forEach(callback => {
561
+ if (callback.onError) callback.onError(err);
562
+ });
563
+ });
564
+ return;
565
+ }
566
+
477
567
  const { errorMessage, errorCode } = extractMessageError(message);
478
568
  const error = new ApiError(errorMessage, errorMessage, errorCode);
479
569
  collectionSub.callbacks.forEach(callback => {
@@ -489,6 +579,42 @@ export class RebaseWebSocketClient {
489
579
  if (entityKey) {
490
580
  const entitySub = this.entitySubscriptions.get(entityKey);
491
581
  if (entitySub) {
582
+ if (this.isAuthError(message)) {
583
+ this.handleAuthFailure().then(refreshed => {
584
+ if (refreshed) {
585
+ const oldBackendId = entitySub.backendSubscriptionId;
586
+ const newBackendId = `entity_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
587
+ entitySub.backendSubscriptionId = newBackendId;
588
+ this.backendToEntityKey.delete(oldBackendId);
589
+ this.backendToEntityKey.set(newBackendId, entityKey);
590
+
591
+ this.sendMessage({
592
+ type: "subscribe_entity",
593
+ payload: {
594
+ ...entitySub.props,
595
+ subscriptionId: newBackendId
596
+ }
597
+ }).catch(error => {
598
+ console.error("[WS] Failed to re-subscribe entity after auth refresh:", entityKey, error);
599
+ entitySub.callbacks.forEach(callback => {
600
+ if (callback.onError) callback.onError(error);
601
+ });
602
+ });
603
+ } else {
604
+ const { errorMessage, errorCode } = extractMessageError(message);
605
+ const error = new ApiError(errorMessage, errorMessage, errorCode);
606
+ entitySub.callbacks.forEach(callback => {
607
+ if (callback.onError) callback.onError(error);
608
+ });
609
+ }
610
+ }).catch(err => {
611
+ entitySub.callbacks.forEach(callback => {
612
+ if (callback.onError) callback.onError(err);
613
+ });
614
+ });
615
+ return;
616
+ }
617
+
492
618
  const { errorMessage, errorCode } = extractMessageError(message);
493
619
  const error = new ApiError(errorMessage, errorMessage, errorCode);
494
620
  entitySub.callbacks.forEach(callback => {