Skip to content

Commit 1691d6d

Browse files
chacha912hackerwins
andcommitted
Enhance type inference in Document.subscribe
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 1691d6d

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)