Skip to content

Commit 9930cf6

Browse files
chacha912hackerwins
andcommitted
Enhance type inference in Document.subscribe (#814)
This commit improves the type inference for the types used in Document.subscribe. Additionally, it removes the code that forces casting in the test code to align with these improvements. --------- Co-authored-by: Hackerwins <susukang98@gmail.com>
1 parent 2e33940 commit 9930cf6

File tree

7 files changed

+154
-88
lines changed

7 files changed

+154
-88
lines changed

src/document/crdt/tree.ts

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -87,16 +87,37 @@ export enum TreeChangeType {
8787
/**
8888
* `TreeChange` represents the change in the tree.
8989
*/
90-
export interface TreeChange {
91-
actor: ActorID;
92-
type: TreeChangeType;
93-
from: number;
94-
to: number;
95-
fromPath: Array<number>;
96-
toPath: Array<number>;
97-
value?: Array<TreeNode> | { [key: string]: any } | Array<string>;
98-
splitLevel?: number;
99-
}
90+
export type TreeChange =
91+
| {
92+
actor: ActorID;
93+
type: TreeChangeType.Content;
94+
from: number;
95+
to: number;
96+
fromPath: Array<number>;
97+
toPath: Array<number>;
98+
value?: Array<TreeNode>;
99+
splitLevel?: number;
100+
}
101+
| {
102+
actor: ActorID;
103+
type: TreeChangeType.Style;
104+
from: number;
105+
to: number;
106+
fromPath: Array<number>;
107+
toPath: Array<number>;
108+
value: { [key: string]: string };
109+
splitLevel?: number;
110+
}
111+
| {
112+
actor: ActorID;
113+
type: TreeChangeType.RemoveStyle;
114+
from: number;
115+
to: number;
116+
fromPath: Array<number>;
117+
toPath: Array<number>;
118+
value?: Array<string>;
119+
splitLevel?: number;
120+
};
100121

101122
/**
102123
* `CRDTTreePos` represent a position in the tree. It is used to identify a
@@ -876,7 +897,6 @@ export class CRDTTree extends CRDTGCElement {
876897
const [toParent, toLeft] = this.findNodesAndSplitText(range[1], editedAt);
877898

878899
const changes: Array<TreeChange> = [];
879-
const value = attributesToRemove ? attributesToRemove : undefined;
880900
this.traverseInPosRange(
881901
fromParent,
882902
fromLeft,
@@ -893,13 +913,13 @@ export class CRDTTree extends CRDTGCElement {
893913
}
894914

895915
changes.push({
916+
actor: editedAt.getActorID()!,
896917
type: TreeChangeType.RemoveStyle,
897918
from: this.toIndex(fromParent, fromLeft),
898919
to: this.toIndex(toParent, toLeft),
899920
fromPath: this.toPath(fromParent, fromLeft),
900921
toPath: this.toPath(toParent, toLeft),
901-
actor: editedAt.getActorID()!,
902-
value,
922+
value: attributesToRemove,
903923
});
904924
}
905925
},

src/document/document.ts

Lines changed: 52 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,28 @@ export interface PresenceChangedEvent<P extends Indexable>
369369
value: { clientID: ActorID; presence: P };
370370
}
371371

372+
type DocEventCallbackMap<P extends Indexable> = {
373+
default: NextFn<
374+
| SnapshotEvent
375+
| LocalChangeEvent<OperationInfo, P>
376+
| RemoteChangeEvent<OperationInfo, P>
377+
>;
378+
presence: NextFn<
379+
| InitializedEvent<P>
380+
| WatchedEvent<P>
381+
| UnwatchedEvent<P>
382+
| PresenceChangedEvent<P>
383+
>;
384+
'my-presence': NextFn<InitializedEvent<P> | PresenceChangedEvent<P>>;
385+
others: NextFn<WatchedEvent<P> | UnwatchedEvent<P> | PresenceChangedEvent<P>>;
386+
connection: NextFn<ConnectionChangedEvent>;
387+
sync: NextFn<SyncStatusChangedEvent>;
388+
all: NextFn<TransactionEvent<P>>;
389+
};
390+
export type DocEventTopic = keyof DocEventCallbackMap<never>;
391+
export type DocEventCallback<P extends Indexable> =
392+
DocEventCallbackMap<P>[DocEventTopic];
393+
372394
/**
373395
* Indexable key, value
374396
* @public
@@ -710,7 +732,7 @@ export class Document<T, P extends Indexable = Indexable> {
710732
* The callback will be called when the document is changed.
711733
*/
712734
public subscribe(
713-
nextOrObserver: Observer<DocEvent> | NextFn<DocEvent>,
735+
next: DocEventCallbackMap<P>['default'],
714736
error?: ErrorFn,
715737
complete?: CompleteFn,
716738
): Unsubscribe;
@@ -721,7 +743,7 @@ export class Document<T, P extends Indexable = Indexable> {
721743
*/
722744
public subscribe(
723745
type: 'presence',
724-
next: NextFn<DocEvent<P>>,
746+
next: DocEventCallbackMap<P>['presence'],
725747
error?: ErrorFn,
726748
complete?: CompleteFn,
727749
): Unsubscribe;
@@ -731,7 +753,7 @@ export class Document<T, P extends Indexable = Indexable> {
731753
*/
732754
public subscribe(
733755
type: 'my-presence',
734-
next: NextFn<DocEvent<P>>,
756+
next: DocEventCallbackMap<P>['my-presence'],
735757
error?: ErrorFn,
736758
complete?: CompleteFn,
737759
): Unsubscribe;
@@ -742,7 +764,7 @@ export class Document<T, P extends Indexable = Indexable> {
742764
*/
743765
public subscribe(
744766
type: 'others',
745-
next: NextFn<DocEvent<P>>,
767+
next: DocEventCallbackMap<P>['others'],
746768
error?: ErrorFn,
747769
complete?: CompleteFn,
748770
): Unsubscribe;
@@ -752,7 +774,7 @@ export class Document<T, P extends Indexable = Indexable> {
752774
*/
753775
public subscribe(
754776
type: 'connection',
755-
next: NextFn<DocEvent<P>>,
777+
next: DocEventCallbackMap<P>['connection'],
756778
error?: ErrorFn,
757779
complete?: CompleteFn,
758780
): Unsubscribe;
@@ -762,7 +784,7 @@ export class Document<T, P extends Indexable = Indexable> {
762784
*/
763785
public subscribe(
764786
type: 'sync',
765-
next: NextFn<DocEvent<P>>,
787+
next: DocEventCallbackMap<P>['sync'],
766788
error?: ErrorFn,
767789
complete?: CompleteFn,
768790
): Unsubscribe;
@@ -775,7 +797,9 @@ export class Document<T, P extends Indexable = Indexable> {
775797
TOperationInfo extends OperationInfoOf<T, TPath>,
776798
>(
777799
targetPath: TPath,
778-
next: NextFn<DocEvent<P, TOperationInfo>>,
800+
next: NextFn<
801+
LocalChangeEvent<TOperationInfo, P> | RemoteChangeEvent<TOperationInfo, P>
802+
>,
779803
error?: ErrorFn,
780804
complete?: CompleteFn,
781805
): Unsubscribe;
@@ -784,7 +808,7 @@ export class Document<T, P extends Indexable = Indexable> {
784808
*/
785809
public subscribe(
786810
type: 'all',
787-
next: NextFn<TransactionEvent<P>>,
811+
next: DocEventCallbackMap<P>['all'],
788812
error?: ErrorFn,
789813
complete?: CompleteFn,
790814
): Unsubscribe;
@@ -795,11 +819,13 @@ export class Document<T, P extends Indexable = Indexable> {
795819
TPath extends PathOf<T>,
796820
TOperationInfo extends OperationInfoOf<T, TPath>,
797821
>(
798-
arg1: TPath | string | Observer<DocEvent<P>> | NextFn<DocEvent<P>>,
822+
arg1: TPath | DocEventTopic | DocEventCallbackMap<P>['default'],
799823
arg2?:
800-
| NextFn<DocEvent<P, TOperationInfo>>
801-
| NextFn<DocEvent<P>>
802-
| NextFn<TransactionEvent<P>>
824+
| NextFn<
825+
| LocalChangeEvent<TOperationInfo, P>
826+
| RemoteChangeEvent<TOperationInfo, P>
827+
>
828+
| DocEventCallback<P>
803829
| ErrorFn,
804830
arg3?: ErrorFn | CompleteFn,
805831
arg4?: CompleteFn,
@@ -809,7 +835,7 @@ export class Document<T, P extends Indexable = Indexable> {
809835
throw new Error('Second argument must be a callback function');
810836
}
811837
if (arg1 === 'presence') {
812-
const callback = arg2 as NextFn<DocEvent<P>>;
838+
const callback = arg2 as DocEventCallbackMap<P>['presence'];
813839
return this.eventStream.subscribe(
814840
(event) => {
815841
for (const docEvent of event) {
@@ -830,21 +856,19 @@ export class Document<T, P extends Indexable = Indexable> {
830856
);
831857
}
832858
if (arg1 === 'my-presence') {
833-
const callback = arg2 as NextFn<DocEvent<P>>;
859+
const callback = arg2 as DocEventCallbackMap<P>['my-presence'];
834860
return this.eventStream.subscribe(
835861
(event) => {
836862
for (const docEvent of event) {
837863
if (
838864
docEvent.type !== DocEventType.Initialized &&
839-
docEvent.type !== DocEventType.Watched &&
840-
docEvent.type !== DocEventType.Unwatched &&
841865
docEvent.type !== DocEventType.PresenceChanged
842866
) {
843867
continue;
844868
}
845869

846870
if (
847-
docEvent.type !== DocEventType.Initialized &&
871+
docEvent.type === DocEventType.PresenceChanged &&
848872
docEvent.value.clientID !== this.changeID.getActorID()
849873
) {
850874
continue;
@@ -858,7 +882,7 @@ export class Document<T, P extends Indexable = Indexable> {
858882
);
859883
}
860884
if (arg1 === 'others') {
861-
const callback = arg2 as NextFn<DocEvent<P>>;
885+
const callback = arg2 as DocEventCallbackMap<P>['others'];
862886
return this.eventStream.subscribe(
863887
(event) => {
864888
for (const docEvent of event) {
@@ -880,7 +904,7 @@ export class Document<T, P extends Indexable = Indexable> {
880904
);
881905
}
882906
if (arg1 === 'connection') {
883-
const callback = arg2 as NextFn<DocEvent<P>>;
907+
const callback = arg2 as DocEventCallbackMap<P>['connection'];
884908
return this.eventStream.subscribe(
885909
(event) => {
886910
for (const docEvent of event) {
@@ -895,7 +919,7 @@ export class Document<T, P extends Indexable = Indexable> {
895919
);
896920
}
897921
if (arg1 === 'sync') {
898-
const callback = arg2 as NextFn<DocEvent<P>>;
922+
const callback = arg2 as DocEventCallbackMap<P>['sync'];
899923
return this.eventStream.subscribe(
900924
(event) => {
901925
for (const docEvent of event) {
@@ -910,31 +934,28 @@ export class Document<T, P extends Indexable = Indexable> {
910934
);
911935
}
912936
if (arg1 === 'all') {
913-
const callback = arg2 as NextFn<TransactionEvent<P>>;
937+
const callback = arg2 as DocEventCallbackMap<P>['all'];
914938
return this.eventStream.subscribe(callback, arg3, arg4);
915939
}
916940
const target = arg1;
917-
const callback = arg2 as NextFn<DocEvent<P>>;
941+
const callback = arg2 as NextFn<
942+
| LocalChangeEvent<TOperationInfo, P>
943+
| RemoteChangeEvent<TOperationInfo, P>
944+
>;
918945
return this.eventStream.subscribe(
919946
(event) => {
920947
for (const docEvent of event) {
921948
if (
922-
docEvent.type !== DocEventType.Snapshot &&
923949
docEvent.type !== DocEventType.LocalChange &&
924950
docEvent.type !== DocEventType.RemoteChange
925951
) {
926952
continue;
927953
}
928954

929-
if (docEvent.type === DocEventType.Snapshot) {
930-
target === '$' && callback(docEvent);
931-
continue;
932-
}
933-
934-
const targetOps: Array<OperationInfo> = [];
955+
const targetOps: Array<TOperationInfo> = [];
935956
for (const op of docEvent.value.operations) {
936957
if (this.isSameElementOrChildOf(op.path, target)) {
937-
targetOps.push(op);
958+
targetOps.push(op as TOperationInfo);
938959
}
939960
}
940961
targetOps.length &&
@@ -949,7 +970,7 @@ export class Document<T, P extends Indexable = Indexable> {
949970
);
950971
}
951972
if (typeof arg1 === 'function') {
952-
const callback = arg1 as NextFn<DocEvent<P>>;
973+
const callback = arg1 as DocEventCallbackMap<P>['default'];
953974
const error = arg2 as ErrorFn;
954975
const complete = arg3 as CompleteFn;
955976
return this.eventStream.subscribe(

src/document/operation/operation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ export type TreeEditOpInfo = {
149149
path: string;
150150
from: number;
151151
to: number;
152-
value: TreeNode;
152+
value?: Array<TreeNode>;
153153
splitLevel: number;
154154
fromPath: Array<number>;
155155
toPath: Array<number>;

test/integration/client_test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -140,15 +140,15 @@ describe.sequential('Client', function () {
140140

141141
const unsub1 = {
142142
syncEvent: d1.subscribe('sync', (event) => {
143-
eventCollectorSync1.add(event.value as DocumentSyncStatus);
143+
eventCollectorSync1.add(event.value);
144144
}),
145145
doc: d1.subscribe((event) => {
146146
eventCollectorD1.add(event.type);
147147
}),
148148
};
149149
const unsub2 = {
150150
syncEvent: d2.subscribe('sync', (event) => {
151-
eventCollectorSync2.add(event.value as DocumentSyncStatus);
151+
eventCollectorSync2.add(event.value);
152152
}),
153153
doc: d2.subscribe((event) => {
154154
eventCollectorD2.add(event.type);
@@ -236,7 +236,7 @@ describe.sequential('Client', function () {
236236
// 02. c2 changes the sync mode to realtime sync mode.
237237
const eventCollector = new EventCollector();
238238
const unsub1 = d2.subscribe('sync', (event) => {
239-
eventCollector.add(event.value as DocumentSyncStatus);
239+
eventCollector.add(event.value);
240240
});
241241
await c2.changeSyncMode(d2, SyncMode.Realtime);
242242
await eventCollector.waitFor(DocumentSyncStatus.Synced); // sync occurs when resuming
@@ -414,7 +414,7 @@ describe.sequential('Client', function () {
414414

415415
const eventCollector = new EventCollector();
416416
const unsub1 = d2.subscribe('sync', (event) => {
417-
eventCollector.add(event.value as DocumentSyncStatus);
417+
eventCollector.add(event.value);
418418
});
419419

420420
// 01. c2 attach the doc with realtime sync mode at first.
@@ -491,7 +491,7 @@ describe.sequential('Client', function () {
491491
// and sync with push-only mode: CP(2, 2) -> CP(3, 2)
492492
const eventCollector = new EventCollector();
493493
const unsub = d1.subscribe('sync', (event) => {
494-
eventCollector.add(event.value as DocumentSyncStatus);
494+
eventCollector.add(event.value);
495495
});
496496
d1.update((root) => {
497497
root.counter.increase(1);
@@ -569,7 +569,7 @@ describe.sequential('Client', function () {
569569
assert.equal(d1.getRoot().tree.toXML(), '<doc><p>12</p><p>34</p></doc>');
570570
assert.equal(d2.getRoot().tree.toXML(), '<doc><p>12</p><p>34</p></doc>');
571571

572-
d1.update((root: any) => {
572+
d1.update((root) => {
573573
root.tree.edit(2, 2, { type: 'text', value: 'a' });
574574
});
575575
await c1.sync();
@@ -595,7 +595,7 @@ describe.sequential('Client', function () {
595595

596596
c2.changeSyncMode(d2, SyncMode.Realtime);
597597

598-
d2.update((root: any) => {
598+
d2.update((root) => {
599599
root.tree.edit(2, 2, { type: 'text', value: 'b' });
600600
});
601601
await eventCollectorD1.waitAndVerifyNthEvent(3, DocEventType.RemoteChange);

0 commit comments

Comments
 (0)