@rebasepro/client 0.7.0 → 0.9.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/LICENSE +21 -0
- package/README.md +10 -10
- package/dist/api-keys.d.ts +1 -0
- package/dist/auth.d.ts +35 -38
- package/dist/collection.d.ts +9 -13
- package/dist/data-proxy.test.d.ts +1 -0
- package/dist/errors.d.ts +9 -0
- package/dist/index.d.ts +41 -21
- package/dist/index.es.js +605 -368
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +616 -379
- package/dist/index.umd.js.map +1 -1
- package/dist/sdk_query_builder.d.ts +63 -0
- package/dist/storage-registry.d.ts +42 -0
- package/dist/storage.d.ts +9 -1
- package/dist/transport.d.ts +2 -6
- package/dist/websocket.d.ts +25 -21
- package/package.json +13 -12
- package/src/api-keys.ts +1 -0
- package/src/auth.ts +188 -64
- package/src/collection.test.ts +94 -5
- package/src/collection.ts +103 -184
- package/src/data-proxy.test.ts +167 -0
- package/src/errors.ts +9 -0
- package/src/index.ts +218 -24
- package/src/reviver.ts +2 -2
- package/src/sdk_query_builder.ts +138 -0
- package/src/storage-registry.ts +102 -0
- package/src/storage.ts +92 -50
- package/src/transport.ts +31 -105
- package/src/websocket.ts +133 -135
package/src/websocket.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
EntityCollection,
|
|
2
|
+
DeleteProps,
|
|
3
|
+
CollectionConfig,
|
|
5
4
|
FetchCollectionProps,
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
FetchOneProps,
|
|
6
|
+
SaveProps,
|
|
8
7
|
WebSocketMessage,
|
|
9
8
|
WebSocketErrorPayload,
|
|
10
9
|
CollectionUpdateMessage,
|
|
11
|
-
|
|
10
|
+
SingleUpdateMessage,
|
|
12
11
|
TableMetadata,
|
|
13
|
-
BranchInfo
|
|
12
|
+
BranchInfo,
|
|
13
|
+
RebaseApiError
|
|
14
14
|
} from "@rebasepro/types";
|
|
15
15
|
import { rebaseReviver } from "./reviver";
|
|
16
16
|
|
|
@@ -44,19 +44,15 @@ export interface RebaseWebSocketConfig {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Low-level realtime WebSocket client.
|
|
49
|
+
*
|
|
50
|
+
* @internal Not a stable app-facing API. `createRebaseClient()` constructs and
|
|
51
|
+
* manages this internally (exposed as `client.ws`, typed by the minimal
|
|
52
|
+
* `RebaseWebSocket` contract in `@rebasepro/types`). It is re-exported from the
|
|
53
|
+
* package root only because the `@rebasepro/client-postgresql` driver
|
|
54
|
+
* instantiates it directly; its surface may change without a major bump.
|
|
55
|
+
*/
|
|
60
56
|
export class RebaseWebSocketClient {
|
|
61
57
|
private websocketUrl: string;
|
|
62
58
|
private ws: WebSocket | null = null;
|
|
@@ -86,23 +82,23 @@ export class RebaseWebSocketClient {
|
|
|
86
82
|
private collectionSubscriptions = new Map<string, {
|
|
87
83
|
backendSubscriptionId: string;
|
|
88
84
|
callbacks: Map<string, {
|
|
89
|
-
onUpdate: (
|
|
85
|
+
onUpdate: (rows: Record<string, unknown>[]) => void;
|
|
90
86
|
onError?: (error: Error) => void;
|
|
91
87
|
}>;
|
|
92
88
|
props: FetchCollectionProps;
|
|
93
|
-
latestData?:
|
|
89
|
+
latestData?: Record<string, unknown>[]; // Cache the latest flat rows
|
|
94
90
|
lastUpdated?: number; // Timestamp for cache invalidation
|
|
95
91
|
isInitialDataReceived?: boolean; // Track if we got initial data
|
|
96
92
|
}>();
|
|
97
93
|
|
|
98
|
-
private
|
|
94
|
+
private singleSubscriptions = new Map<string, {
|
|
99
95
|
backendSubscriptionId: string;
|
|
100
96
|
callbacks: Map<string, {
|
|
101
|
-
onUpdate: (
|
|
97
|
+
onUpdate: (row: Record<string, unknown> | null) => void;
|
|
102
98
|
onError?: (error: Error) => void;
|
|
103
99
|
}>;
|
|
104
|
-
props:
|
|
105
|
-
latestData?:
|
|
100
|
+
props: FetchOneProps;
|
|
101
|
+
latestData?: Record<string, unknown> | null; // Cache the latest flat row
|
|
106
102
|
lastUpdated?: number; // Timestamp for cache invalidation
|
|
107
103
|
isInitialDataReceived?: boolean; // Track if we got initial data
|
|
108
104
|
}>();
|
|
@@ -294,7 +290,7 @@ export class RebaseWebSocketClient {
|
|
|
294
290
|
request.message._queuedReject = request.reject;
|
|
295
291
|
this.messageQueue.push(request.message);
|
|
296
292
|
} else {
|
|
297
|
-
request.reject(new
|
|
293
|
+
request.reject(new RebaseApiError("Connection closed"));
|
|
298
294
|
}
|
|
299
295
|
this.pendingRequests.delete(reqId);
|
|
300
296
|
}
|
|
@@ -380,7 +376,7 @@ export class RebaseWebSocketClient {
|
|
|
380
376
|
}
|
|
381
377
|
|
|
382
378
|
/**
|
|
383
|
-
* Shared logic for re-subscribing a collection or
|
|
379
|
+
* Shared logic for re-subscribing a collection or row subscription
|
|
384
380
|
* after an auth error is resolved by refreshing credentials.
|
|
385
381
|
*/
|
|
386
382
|
private resubscribeAfterAuthRefresh(
|
|
@@ -388,12 +384,12 @@ export class RebaseWebSocketClient {
|
|
|
388
384
|
subscription: {
|
|
389
385
|
backendSubscriptionId: string;
|
|
390
386
|
callbacks: Map<string, { onUpdate: (...args: never[]) => void; onError?: (error: Error) => void }>;
|
|
391
|
-
props: FetchCollectionProps |
|
|
387
|
+
props: FetchCollectionProps | FetchOneProps;
|
|
392
388
|
},
|
|
393
389
|
subscriptionKey: string,
|
|
394
|
-
idPrefix: "collection" | "
|
|
390
|
+
idPrefix: "collection" | "row",
|
|
395
391
|
backendKeyMap: Map<string, string>,
|
|
396
|
-
messageType: "subscribe_collection" | "
|
|
392
|
+
messageType: "subscribe_collection" | "subscribe_one"
|
|
397
393
|
): void {
|
|
398
394
|
this.handleAuthFailure().then(refreshed => {
|
|
399
395
|
if (refreshed) {
|
|
@@ -417,7 +413,7 @@ export class RebaseWebSocketClient {
|
|
|
417
413
|
});
|
|
418
414
|
} else {
|
|
419
415
|
const { errorMessage, errorCode } = extractMessageError(message);
|
|
420
|
-
const error = new
|
|
416
|
+
const error = new RebaseApiError(errorMessage, { code: errorCode });
|
|
421
417
|
subscription.callbacks.forEach(callback => {
|
|
422
418
|
if (callback.onError) callback.onError(error);
|
|
423
419
|
});
|
|
@@ -447,7 +443,7 @@ export class RebaseWebSocketClient {
|
|
|
447
443
|
this.doSendMessage(pendingReq.message, pendingReq.resolve, pendingReq.reject).catch(pendingReq.reject);
|
|
448
444
|
} else {
|
|
449
445
|
const { errorMessage, errorCode } = extractMessageError(message);
|
|
450
|
-
pendingReq.reject(new
|
|
446
|
+
pendingReq.reject(new RebaseApiError(errorMessage, { code: errorCode }));
|
|
451
447
|
}
|
|
452
448
|
}).catch(err => {
|
|
453
449
|
pendingReq.reject(err);
|
|
@@ -455,7 +451,7 @@ export class RebaseWebSocketClient {
|
|
|
455
451
|
} else {
|
|
456
452
|
this.pendingRequests.delete(requestId);
|
|
457
453
|
const { errorMessage, errorCode } = extractMessageError(message);
|
|
458
|
-
pendingReq.reject(new
|
|
454
|
+
pendingReq.reject(new RebaseApiError(errorMessage, { code: errorCode }));
|
|
459
455
|
}
|
|
460
456
|
} else {
|
|
461
457
|
this.pendingRequests.delete(requestId);
|
|
@@ -470,23 +466,24 @@ export class RebaseWebSocketClient {
|
|
|
470
466
|
if (subscriptionKey) {
|
|
471
467
|
const collectionSub = this.collectionSubscriptions.get(subscriptionKey);
|
|
472
468
|
if (collectionSub) {
|
|
473
|
-
const
|
|
469
|
+
const wireEntities = (message.rows || []) as unknown as Record<string, unknown>[];
|
|
470
|
+
const incomingRows = wireEntities;
|
|
474
471
|
|
|
475
|
-
// Structural merge: preserve cached
|
|
472
|
+
// Structural merge: preserve cached row references for rows
|
|
476
473
|
// whose values haven't changed. This prevents downstream React components
|
|
477
474
|
// from re-rendering (VirtualTableCell uses deepEqual on rowData —
|
|
478
475
|
// same reference = instant true, avoiding expensive deep comparison).
|
|
479
|
-
const
|
|
476
|
+
const rows = this.mergeRows(collectionSub.latestData, incomingRows);
|
|
480
477
|
|
|
481
478
|
// Cache the latest data with optimizations
|
|
482
|
-
collectionSub.latestData =
|
|
479
|
+
collectionSub.latestData = rows;
|
|
483
480
|
collectionSub.lastUpdated = Date.now();
|
|
484
481
|
collectionSub.isInitialDataReceived = true;
|
|
485
482
|
|
|
486
483
|
// Notify all callbacks for this subscription
|
|
487
484
|
collectionSub.callbacks.forEach(callback => {
|
|
488
485
|
try {
|
|
489
|
-
callback.onUpdate(
|
|
486
|
+
callback.onUpdate(rows);
|
|
490
487
|
} catch (error) {
|
|
491
488
|
console.error("Error in collection subscription callback:", error);
|
|
492
489
|
if (callback.onError) {
|
|
@@ -499,30 +496,31 @@ export class RebaseWebSocketClient {
|
|
|
499
496
|
}
|
|
500
497
|
}
|
|
501
498
|
|
|
502
|
-
// Handle instant
|
|
499
|
+
// Handle instant row-level patches for collection subscriptions.
|
|
503
500
|
// These arrive before the full refetch and give immediate cross-tab feedback.
|
|
504
|
-
if (subscriptionId && type === "
|
|
501
|
+
if (subscriptionId && type === "collection_patch") {
|
|
505
502
|
const subscriptionKey = this.backendToCollectionKey.get(subscriptionId);
|
|
506
503
|
if (subscriptionKey) {
|
|
507
504
|
const collectionSub = this.collectionSubscriptions.get(subscriptionKey);
|
|
508
505
|
if (collectionSub && collectionSub.isInitialDataReceived && collectionSub.latestData) {
|
|
509
|
-
const
|
|
510
|
-
const patchEntityId = (message as unknown as {
|
|
511
|
-
|
|
506
|
+
const patchWireEntity = message.row ?? null;
|
|
507
|
+
const patchEntityId = (message as unknown as { id: string }).id;
|
|
508
|
+
const patchRow = patchWireEntity ? (patchWireEntity as unknown as Record<string, unknown>) : null;
|
|
509
|
+
let updated: Record<string, unknown>[];
|
|
512
510
|
|
|
513
|
-
if (
|
|
514
|
-
//
|
|
511
|
+
if (patchRow === null) {
|
|
512
|
+
// Row was deleted — remove it from the cached list
|
|
515
513
|
updated = collectionSub.latestData.filter(e => String(e.id) !== String(patchEntityId));
|
|
516
514
|
} else {
|
|
517
|
-
//
|
|
518
|
-
const idx = collectionSub.latestData.findIndex(e => String(e.id) === String(
|
|
515
|
+
// Row was created or updated — merge into the cached list
|
|
516
|
+
const idx = collectionSub.latestData.findIndex(e => String(e.id) === String(patchRow.id));
|
|
519
517
|
if (idx >= 0) {
|
|
520
518
|
// Update in place (preserve array position)
|
|
521
519
|
updated = [...collectionSub.latestData];
|
|
522
|
-
updated[idx] =
|
|
520
|
+
updated[idx] = patchRow;
|
|
523
521
|
} else {
|
|
524
|
-
// New
|
|
525
|
-
updated = [
|
|
522
|
+
// New row — prepend (most recently created first)
|
|
523
|
+
updated = [patchRow, ...collectionSub.latestData];
|
|
526
524
|
}
|
|
527
525
|
}
|
|
528
526
|
|
|
@@ -545,24 +543,25 @@ export class RebaseWebSocketClient {
|
|
|
545
543
|
}
|
|
546
544
|
}
|
|
547
545
|
|
|
548
|
-
// Handle subscription updates for
|
|
549
|
-
if (subscriptionId && type === "
|
|
546
|
+
// Handle subscription updates for row subscriptions
|
|
547
|
+
if (subscriptionId && type === "single_update") {
|
|
550
548
|
const subscriptionKey = this.backendToEntityKey.get(subscriptionId);
|
|
551
549
|
if (subscriptionKey) {
|
|
552
|
-
const entitySub = this.
|
|
550
|
+
const entitySub = this.singleSubscriptions.get(subscriptionKey);
|
|
553
551
|
if (entitySub) {
|
|
554
|
-
const
|
|
552
|
+
const wireEntity = message.row ?? null;
|
|
553
|
+
const row = wireEntity ? (wireEntity as unknown as Record<string, unknown>) : null;
|
|
555
554
|
// Cache the latest data with optimizations
|
|
556
|
-
entitySub.latestData =
|
|
555
|
+
entitySub.latestData = row;
|
|
557
556
|
entitySub.lastUpdated = Date.now();
|
|
558
557
|
entitySub.isInitialDataReceived = true;
|
|
559
558
|
|
|
560
559
|
// Notify all callbacks for this subscription
|
|
561
560
|
entitySub.callbacks.forEach(callback => {
|
|
562
561
|
try {
|
|
563
|
-
callback.onUpdate(
|
|
562
|
+
callback.onUpdate(row);
|
|
564
563
|
} catch (error) {
|
|
565
|
-
console.error("Error in
|
|
564
|
+
console.error("Error in row subscription callback:", error);
|
|
566
565
|
if (callback.onError) {
|
|
567
566
|
callback.onError(error instanceof Error ? error : new Error(String(error)));
|
|
568
567
|
}
|
|
@@ -592,7 +591,7 @@ export class RebaseWebSocketClient {
|
|
|
592
591
|
}
|
|
593
592
|
|
|
594
593
|
const { errorMessage, errorCode } = extractMessageError(message);
|
|
595
|
-
const error = new
|
|
594
|
+
const error = new RebaseApiError(errorMessage, { code: errorCode });
|
|
596
595
|
collectionSub.callbacks.forEach(callback => {
|
|
597
596
|
if (callback.onError) {
|
|
598
597
|
callback.onError(error);
|
|
@@ -604,22 +603,22 @@ export class RebaseWebSocketClient {
|
|
|
604
603
|
|
|
605
604
|
const entityKey = this.backendToEntityKey.get(subscriptionId);
|
|
606
605
|
if (entityKey) {
|
|
607
|
-
const entitySub = this.
|
|
606
|
+
const entitySub = this.singleSubscriptions.get(entityKey);
|
|
608
607
|
if (entitySub) {
|
|
609
608
|
if (this.isAuthError(message)) {
|
|
610
609
|
this.resubscribeAfterAuthRefresh(
|
|
611
610
|
message,
|
|
612
611
|
entitySub,
|
|
613
612
|
entityKey,
|
|
614
|
-
"
|
|
613
|
+
"row",
|
|
615
614
|
this.backendToEntityKey,
|
|
616
|
-
"
|
|
615
|
+
"subscribe_one"
|
|
617
616
|
);
|
|
618
617
|
return;
|
|
619
618
|
}
|
|
620
619
|
|
|
621
620
|
const { errorMessage, errorCode } = extractMessageError(message);
|
|
622
|
-
const error = new
|
|
621
|
+
const error = new RebaseApiError(errorMessage, { code: errorCode });
|
|
623
622
|
entitySub.callbacks.forEach(callback => {
|
|
624
623
|
if (callback.onError) {
|
|
625
624
|
callback.onError(error);
|
|
@@ -639,7 +638,7 @@ export class RebaseWebSocketClient {
|
|
|
639
638
|
if (message.type === "ERROR" || message.error) {
|
|
640
639
|
if (callback.onError) {
|
|
641
640
|
const { errorMessage, errorCode } = extractMessageError(message);
|
|
642
|
-
callback.onError(new
|
|
641
|
+
callback.onError(new RebaseApiError(errorMessage, { code: errorCode }));
|
|
643
642
|
}
|
|
644
643
|
} else {
|
|
645
644
|
callback.onUpdate(message);
|
|
@@ -747,7 +746,7 @@ export class RebaseWebSocketClient {
|
|
|
747
746
|
await this.ensureAuthenticated();
|
|
748
747
|
} catch (error: unknown) {
|
|
749
748
|
const errorMessage = error instanceof Error ? error.message : "Authentication required";
|
|
750
|
-
reject(new
|
|
749
|
+
reject(new RebaseApiError(errorMessage));
|
|
751
750
|
return;
|
|
752
751
|
}
|
|
753
752
|
}
|
|
@@ -757,7 +756,7 @@ export class RebaseWebSocketClient {
|
|
|
757
756
|
|
|
758
757
|
const expectsResponse = ![
|
|
759
758
|
"subscribe_collection",
|
|
760
|
-
"
|
|
759
|
+
"subscribe_one",
|
|
761
760
|
"unsubscribe",
|
|
762
761
|
"join_channel",
|
|
763
762
|
"leave_channel",
|
|
@@ -771,7 +770,7 @@ export class RebaseWebSocketClient {
|
|
|
771
770
|
const timeoutHandle = setTimeout(() => {
|
|
772
771
|
if (this.pendingRequests.has(requestId)) {
|
|
773
772
|
this.pendingRequests.delete(requestId);
|
|
774
|
-
reject(new
|
|
773
|
+
reject(new RebaseApiError("Request timed out"));
|
|
775
774
|
}
|
|
776
775
|
}, this.requestTimeoutMs);
|
|
777
776
|
|
|
@@ -797,38 +796,39 @@ export class RebaseWebSocketClient {
|
|
|
797
796
|
if (expectsResponse) {
|
|
798
797
|
this.pendingRequests.delete(requestId);
|
|
799
798
|
}
|
|
800
|
-
reject(new
|
|
799
|
+
reject(new RebaseApiError("Failed to send message", { cause: error }));
|
|
801
800
|
}
|
|
802
801
|
}
|
|
803
802
|
|
|
804
803
|
// Data source methods
|
|
805
|
-
async fetchCollection<M extends Record<string, unknown>>(props: FetchCollectionProps<M>): Promise<
|
|
804
|
+
async fetchCollection<M extends Record<string, unknown>>(props: FetchCollectionProps<M>): Promise<Record<string, unknown>[]> {
|
|
806
805
|
const response = await this.sendMessage({
|
|
807
806
|
type: "FETCH_COLLECTION",
|
|
808
807
|
payload: props
|
|
809
|
-
}) as {
|
|
810
|
-
return response.
|
|
808
|
+
}) as { rows?: Record<string, unknown>[] };
|
|
809
|
+
return (response.rows || []);
|
|
811
810
|
}
|
|
812
811
|
|
|
813
|
-
async
|
|
812
|
+
async fetchOne<M extends Record<string, unknown>>(props: FetchOneProps<M>): Promise<Record<string, unknown> | undefined> {
|
|
814
813
|
const response = await this.sendMessage({
|
|
815
|
-
type: "
|
|
814
|
+
type: "FETCH_ONE",
|
|
816
815
|
payload: props
|
|
817
|
-
}) as {
|
|
818
|
-
|
|
816
|
+
}) as { row?: Record<string, unknown> };
|
|
817
|
+
const wireEntity = response.row;
|
|
818
|
+
return wireEntity ?? undefined;
|
|
819
819
|
}
|
|
820
820
|
|
|
821
|
-
async
|
|
821
|
+
async save<M extends Record<string, unknown>>(props: SaveProps<M>): Promise<Record<string, unknown>> {
|
|
822
822
|
const response = await this.sendMessage({
|
|
823
|
-
type: "
|
|
823
|
+
type: "SAVE",
|
|
824
824
|
payload: props
|
|
825
|
-
}) as {
|
|
826
|
-
return response.
|
|
825
|
+
}) as { row: Record<string, unknown> };
|
|
826
|
+
return response.row;
|
|
827
827
|
}
|
|
828
828
|
|
|
829
|
-
async
|
|
829
|
+
async delete<M extends Record<string, unknown>>(props: DeleteProps<M>): Promise<void> {
|
|
830
830
|
await this.sendMessage({
|
|
831
|
-
type: "
|
|
831
|
+
type: "DELETE",
|
|
832
832
|
payload: props
|
|
833
833
|
});
|
|
834
834
|
}
|
|
@@ -864,23 +864,23 @@ options }
|
|
|
864
864
|
return response.database;
|
|
865
865
|
}
|
|
866
866
|
|
|
867
|
-
async checkUniqueField(path: string, name: string, value: unknown,
|
|
867
|
+
async checkUniqueField(path: string, name: string, value: unknown, id?: string, collection?: CollectionConfig): Promise<boolean> {
|
|
868
868
|
const response = await this.sendMessage({
|
|
869
869
|
type: "CHECK_UNIQUE_FIELD",
|
|
870
870
|
payload: {
|
|
871
871
|
path,
|
|
872
872
|
name,
|
|
873
873
|
value,
|
|
874
|
-
|
|
874
|
+
id,
|
|
875
875
|
collection
|
|
876
876
|
}
|
|
877
877
|
}) as { isUnique: boolean };
|
|
878
878
|
return response.isUnique;
|
|
879
879
|
}
|
|
880
880
|
|
|
881
|
-
async
|
|
881
|
+
async count<M extends Record<string, unknown>>(props: FetchCollectionProps<M>): Promise<number> {
|
|
882
882
|
const response = await this.sendMessage({
|
|
883
|
-
type: "
|
|
883
|
+
type: "COUNT",
|
|
884
884
|
payload: props
|
|
885
885
|
}) as { count: number };
|
|
886
886
|
return response.count;
|
|
@@ -1017,52 +1017,50 @@ options }
|
|
|
1017
1017
|
}
|
|
1018
1018
|
|
|
1019
1019
|
/**
|
|
1020
|
-
* Merge incoming
|
|
1021
|
-
* for
|
|
1022
|
-
* React re-renders when the server refetches all
|
|
1020
|
+
* Merge incoming rows with cached data, preserving cached references
|
|
1021
|
+
* for rows whose values haven't changed. This avoids unnecessary
|
|
1022
|
+
* React re-renders when the server refetches all rows but most
|
|
1023
1023
|
* haven't actually changed.
|
|
1024
1024
|
*/
|
|
1025
|
-
private
|
|
1025
|
+
private mergeRows(cached: Record<string, unknown>[] | undefined, incoming: Record<string, unknown>[]): Record<string, unknown>[] {
|
|
1026
1026
|
if (!cached || cached.length === 0) return incoming;
|
|
1027
1027
|
|
|
1028
|
-
// Build a lookup from cached
|
|
1029
|
-
const cachedById = new Map<string | number,
|
|
1030
|
-
for (const
|
|
1031
|
-
cachedById.set(
|
|
1028
|
+
// Build a lookup from cached rows by ID for O(1) access
|
|
1029
|
+
const cachedById = new Map<string | number, Record<string, unknown>>();
|
|
1030
|
+
for (const row of cached) {
|
|
1031
|
+
cachedById.set(row.id as string | number, row);
|
|
1032
1032
|
}
|
|
1033
1033
|
|
|
1034
|
-
return incoming.map(
|
|
1035
|
-
const
|
|
1036
|
-
if (!
|
|
1034
|
+
return incoming.map(incomingRow => {
|
|
1035
|
+
const cachedRow = cachedById.get(incomingRow.id as string | number);
|
|
1036
|
+
if (!cachedRow) return incomingRow;
|
|
1037
1037
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1038
|
+
// Compare flat rows directly (no more path/values nesting)
|
|
1039
|
+
const normCached = this.normalizeForComparison(cachedRow) as Record<string, unknown>;
|
|
1040
|
+
const normIncoming = this.normalizeForComparison(incomingRow) as Record<string, unknown>;
|
|
1041
1041
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
mismatches[key] = { cached: normCached[key],
|
|
1042
|
+
if (this.deepEqual(normCached, normIncoming)) {
|
|
1043
|
+
return cachedRow;
|
|
1044
|
+
} else {
|
|
1045
|
+
// Deep debug: Why did it fail?
|
|
1046
|
+
const mismatches: Record<string, { cached: unknown, incoming: unknown }> = {};
|
|
1047
|
+
const allKeys = new Set([...Object.keys(normCached), ...Object.keys(normIncoming)]);
|
|
1048
|
+
for (const key of allKeys) {
|
|
1049
|
+
if (!this.deepEqual(normCached[key], normIncoming[key])) {
|
|
1050
|
+
mismatches[key] = { cached: normCached[key],
|
|
1052
1051
|
incoming: normIncoming[key] };
|
|
1053
|
-
}
|
|
1054
1052
|
}
|
|
1055
|
-
console.debug(`[RebaseWS] Row ${incomingEntity.id} refetch mismatch:\n`, JSON.stringify(mismatches, null, 2));
|
|
1056
1053
|
}
|
|
1054
|
+
console.debug(`[RebaseWS] Row ${incomingRow.id} refetch mismatch:\n`, JSON.stringify(mismatches, null, 2));
|
|
1057
1055
|
}
|
|
1058
|
-
return
|
|
1056
|
+
return incomingRow;
|
|
1059
1057
|
});
|
|
1060
1058
|
}
|
|
1061
1059
|
|
|
1062
1060
|
// Subscription methods
|
|
1063
1061
|
listenCollection<M extends Record<string, unknown>>(
|
|
1064
1062
|
props: FetchCollectionProps<M>,
|
|
1065
|
-
onUpdate: (
|
|
1063
|
+
onUpdate: (rows: Record<string, unknown>[]) => void,
|
|
1066
1064
|
onError?: (error: Error) => void
|
|
1067
1065
|
): () => void {
|
|
1068
1066
|
const subscriptionKey = this.createCollectionSubscriptionKey(props);
|
|
@@ -1074,7 +1072,7 @@ incoming: normIncoming[key] };
|
|
|
1074
1072
|
if (existingSubscription) {
|
|
1075
1073
|
// Reuse existing subscription - just add the new callback
|
|
1076
1074
|
const callbackMap = existingSubscription.callbacks as Map<string, {
|
|
1077
|
-
onUpdate: (
|
|
1075
|
+
onUpdate: (rows: Record<string, unknown>[]) => void;
|
|
1078
1076
|
onError?: (error: Error) => void;
|
|
1079
1077
|
}>;
|
|
1080
1078
|
callbackMap.set(callbackId, { onUpdate,
|
|
@@ -1112,7 +1110,7 @@ onError });
|
|
|
1112
1110
|
// Create new subscription
|
|
1113
1111
|
const backendSubscriptionId = `collection_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1114
1112
|
const callbackMap = new Map<string, {
|
|
1115
|
-
onUpdate: (
|
|
1113
|
+
onUpdate: (rows: Record<string, unknown>[]) => void;
|
|
1116
1114
|
onError?: (error: Error) => void;
|
|
1117
1115
|
}>();
|
|
1118
1116
|
callbackMap.set(callbackId, { onUpdate,
|
|
@@ -1158,21 +1156,21 @@ onError });
|
|
|
1158
1156
|
};
|
|
1159
1157
|
}
|
|
1160
1158
|
|
|
1161
|
-
|
|
1162
|
-
props:
|
|
1163
|
-
onUpdate: (
|
|
1159
|
+
listenOne<M extends Record<string, unknown>>(
|
|
1160
|
+
props: FetchOneProps<M>,
|
|
1161
|
+
onUpdate: (row: Record<string, unknown> | null) => void,
|
|
1164
1162
|
onError?: (error: Error) => void
|
|
1165
1163
|
): () => void {
|
|
1166
|
-
const subscriptionKey = this.
|
|
1164
|
+
const subscriptionKey = this.createSingleSubscriptionKey(props);
|
|
1167
1165
|
const callbackId = `callback_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1168
1166
|
|
|
1169
1167
|
// Check if we already have a subscription for these exact parameters
|
|
1170
|
-
const existingSubscription = this.
|
|
1168
|
+
const existingSubscription = this.singleSubscriptions.get(subscriptionKey);
|
|
1171
1169
|
|
|
1172
1170
|
if (existingSubscription) {
|
|
1173
1171
|
// Reuse existing subscription - just add the new callback
|
|
1174
1172
|
const callbackMap = existingSubscription.callbacks as Map<string, {
|
|
1175
|
-
onUpdate: (
|
|
1173
|
+
onUpdate: (row: Record<string, unknown> | null) => void;
|
|
1176
1174
|
onError?: (error: Error) => void;
|
|
1177
1175
|
}>;
|
|
1178
1176
|
callbackMap.set(callbackId, { onUpdate,
|
|
@@ -1183,7 +1181,7 @@ onError });
|
|
|
1183
1181
|
try {
|
|
1184
1182
|
onUpdate(existingSubscription.latestData);
|
|
1185
1183
|
} catch (error) {
|
|
1186
|
-
console.error("Error in
|
|
1184
|
+
console.error("Error in row subscription callback:", error);
|
|
1187
1185
|
if (onError) {
|
|
1188
1186
|
onError(error instanceof Error ? error : new Error(String(error)));
|
|
1189
1187
|
}
|
|
@@ -1195,7 +1193,7 @@ onError });
|
|
|
1195
1193
|
callbackMap.delete(callbackId);
|
|
1196
1194
|
if (callbackMap.size === 0) {
|
|
1197
1195
|
// No more callbacks, unsubscribe from backend
|
|
1198
|
-
this.
|
|
1196
|
+
this.singleSubscriptions.delete(subscriptionKey);
|
|
1199
1197
|
this.backendToEntityKey.delete(existingSubscription.backendSubscriptionId);
|
|
1200
1198
|
if (this.isConnected && this.ws) {
|
|
1201
1199
|
this.sendMessage({
|
|
@@ -1210,13 +1208,13 @@ onError });
|
|
|
1210
1208
|
// Create new subscription
|
|
1211
1209
|
const backendSubscriptionId = `entity_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1212
1210
|
const callbackMap = new Map<string, {
|
|
1213
|
-
onUpdate: (
|
|
1211
|
+
onUpdate: (row: Record<string, unknown> | null) => void;
|
|
1214
1212
|
onError?: (error: Error) => void;
|
|
1215
1213
|
}>();
|
|
1216
1214
|
callbackMap.set(callbackId, { onUpdate,
|
|
1217
1215
|
onError });
|
|
1218
1216
|
|
|
1219
|
-
this.
|
|
1217
|
+
this.singleSubscriptions.set(subscriptionKey, {
|
|
1220
1218
|
backendSubscriptionId,
|
|
1221
1219
|
callbacks: callbackMap,
|
|
1222
1220
|
props
|
|
@@ -1227,7 +1225,7 @@ onError });
|
|
|
1227
1225
|
|
|
1228
1226
|
// Send subscription request to backend
|
|
1229
1227
|
this.sendMessage({
|
|
1230
|
-
type: "
|
|
1228
|
+
type: "subscribe_one",
|
|
1231
1229
|
payload: {
|
|
1232
1230
|
...props,
|
|
1233
1231
|
subscriptionId: backendSubscriptionId
|
|
@@ -1238,12 +1236,12 @@ onError });
|
|
|
1238
1236
|
|
|
1239
1237
|
// Return unsubscribe function
|
|
1240
1238
|
return () => {
|
|
1241
|
-
const subscription = this.
|
|
1239
|
+
const subscription = this.singleSubscriptions.get(subscriptionKey);
|
|
1242
1240
|
if (subscription) {
|
|
1243
1241
|
const callbacks = subscription.callbacks;
|
|
1244
1242
|
callbacks.delete(callbackId);
|
|
1245
1243
|
if (callbacks.size === 0) {
|
|
1246
|
-
this.
|
|
1244
|
+
this.singleSubscriptions.delete(subscriptionKey);
|
|
1247
1245
|
this.backendToEntityKey.delete(subscription.backendSubscriptionId);
|
|
1248
1246
|
if (this.isConnected && this.ws) {
|
|
1249
1247
|
this.sendMessage({
|
|
@@ -1262,7 +1260,7 @@ onError });
|
|
|
1262
1260
|
* we need to re-register everything to resume receiving updates.
|
|
1263
1261
|
*/
|
|
1264
1262
|
private resubscribeAll(): void {
|
|
1265
|
-
console.debug(`[WS] Re-subscribing: ${this.collectionSubscriptions.size} collection(s), ${this.
|
|
1263
|
+
console.debug(`[WS] Re-subscribing: ${this.collectionSubscriptions.size} collection(s), ${this.singleSubscriptions.size} row(ies)`);
|
|
1266
1264
|
|
|
1267
1265
|
// Re-subscribe collection subscriptions
|
|
1268
1266
|
for (const [key, sub] of this.collectionSubscriptions.entries()) {
|
|
@@ -1286,8 +1284,8 @@ onError });
|
|
|
1286
1284
|
});
|
|
1287
1285
|
}
|
|
1288
1286
|
|
|
1289
|
-
// Re-subscribe
|
|
1290
|
-
for (const [key, sub] of this.
|
|
1287
|
+
// Re-subscribe row subscriptions
|
|
1288
|
+
for (const [key, sub] of this.singleSubscriptions.entries()) {
|
|
1291
1289
|
const oldBackendId = sub.backendSubscriptionId;
|
|
1292
1290
|
const newBackendId = `entity_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1293
1291
|
sub.backendSubscriptionId = newBackendId;
|
|
@@ -1296,13 +1294,13 @@ onError });
|
|
|
1296
1294
|
this.backendToEntityKey.set(newBackendId, key);
|
|
1297
1295
|
|
|
1298
1296
|
this.sendMessage({
|
|
1299
|
-
type: "
|
|
1297
|
+
type: "subscribe_one",
|
|
1300
1298
|
payload: {
|
|
1301
1299
|
...sub.props,
|
|
1302
1300
|
subscriptionId: newBackendId
|
|
1303
1301
|
}
|
|
1304
1302
|
}).catch(error => {
|
|
1305
|
-
console.error("[WS] Failed to re-subscribe
|
|
1303
|
+
console.error("[WS] Failed to re-subscribe row:", key, error);
|
|
1306
1304
|
});
|
|
1307
1305
|
}
|
|
1308
1306
|
}
|
|
@@ -1331,7 +1329,7 @@ onError });
|
|
|
1331
1329
|
});
|
|
1332
1330
|
}
|
|
1333
1331
|
|
|
1334
|
-
private
|
|
1335
|
-
return `${props.path}|${props.
|
|
1332
|
+
private createSingleSubscriptionKey(props: FetchOneProps): string {
|
|
1333
|
+
return `${props.path}|${props.id}`;
|
|
1336
1334
|
}
|
|
1337
1335
|
}
|