@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/README.md +164 -0
- package/dist/admin.d.ts +1 -31
- package/dist/auth.d.ts +8 -0
- package/dist/collection.d.ts +4 -3
- package/dist/index.es.js +315 -98
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +323 -97
- package/dist/index.umd.js.map +1 -1
- package/dist/query_builder.d.ts +1 -1
- package/dist/websocket.d.ts +6 -0
- package/package.json +4 -4
- package/src/admin.ts +1 -41
- package/src/auth.ts +64 -0
- package/src/collection.ts +112 -68
- package/src/index.ts +39 -9
- package/src/query_builder.ts +1 -1
- package/src/transport.ts +44 -10
- package/src/websocket.ts +135 -9
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
|
-
|
|
355
|
-
|
|
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
|
-
|
|
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 => {
|