Skip to content

Commit 501b9fa

Browse files
committed
Improve collab client listener interface
1 parent 75ed3af commit 501b9fa

3 files changed

Lines changed: 63 additions & 35 deletions

File tree

.yarn/versions/7415baa1.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
declined:
2+
- "@pitter-patter/collab-client"

packages/collab-client/src/index.ts

Lines changed: 60 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,33 @@ export { receiveCommitTransaction, getVersion, Commit, type CommitJSON, type Nod
1212

1313
export { collab, collabKey } from "./plugin";
1414

15+
export interface CommitsListener {
16+
listen: (
17+
version: number,
18+
options?: { signal?: AbortSignal },
19+
) => AsyncIterableIterator<CommitJSON[]>;
20+
}
21+
1522
export interface CollabClientConfig {
1623
sendCommit: (commit: Commit) => Promise<void>;
17-
getCommits: (version: number, options?: { signal?: AbortSignal }) => Promise<CommitJSON[]>;
24+
listener: CommitsListener;
1825
receiveCommits: (commits: Commit[]) => void;
1926
}
2027

2128
export class CollabClient {
2229
private sending: null | string = null;
2330
private version: number | undefined = undefined;
2431
private seen = new Set<string>();
32+
private controller = new AbortController();
2533

2634
private sendCommit: CollabClientConfig["sendCommit"];
27-
private getCommits: CollabClientConfig["getCommits"];
35+
private listener: CollabClientConfig["listener"];
2836
private receiveCommits: CollabClientConfig["receiveCommits"];
2937

3038
constructor(config: CollabClientConfig) {
3139
this.sendCommit = config.sendCommit;
3240
this.receiveCommits = config.receiveCommits;
33-
this.getCommits = config.getCommits;
41+
this.listener = config.listener;
3442
}
3543

3644
async send(editorState: EditorState) {
@@ -52,33 +60,34 @@ export class CollabClient {
5260
}
5361
}
5462

55-
async listen(editorState: EditorState, signal: AbortSignal) {
63+
update(config: Partial<Omit<CollabClientConfig, "listener">>) {
64+
if (config.sendCommit) this.sendCommit = config.sendCommit;
65+
if (config.receiveCommits) this.receiveCommits = config.receiveCommits;
66+
}
67+
68+
async listen(editorState: EditorState, signal?: AbortSignal) {
5669
this.version = getVersion(editorState);
5770

5871
if (this.version === undefined) {
5972
throw new Error("EditorState is missing the collab plugin, unable to listen for changes");
6073
}
6174

62-
while (!signal.aborted) {
63-
try {
64-
const commitJSONs = await this.getCommits(this.version, { signal });
65-
const commits = commitJSONs.map((json) => Commit.FromJSON(editorState.schema, json));
75+
const getCommitsSignal = AbortSignal.any([...(signal ? [signal] : []), this.controller.signal]);
6676

67-
// Ensure that we don't process the same commit multiple times
68-
const newCommits = commits.filter((commit) => !this.seen.has(commit.ref));
69-
const lastCommit = newCommits[newCommits.length - 1];
70-
if (!lastCommit) continue;
71-
this.version = lastCommit.version;
72-
newCommits.forEach((commit) => this.seen.add(commit.ref));
77+
for await (const commitJSONs of this.listener.listen(this.version, {
78+
signal: getCommitsSignal,
79+
})) {
80+
if (getCommitsSignal.aborted) break;
81+
const commits = commitJSONs.map((json) => Commit.FromJSON(editorState.schema, json));
7382

74-
this.receiveCommits(newCommits);
75-
} catch (e) {
76-
// TODO: Implement a backoff strategy
77-
console.error(e);
78-
await new Promise<void>((resolve) => {
79-
setTimeout(() => resolve(), 3_000);
80-
});
81-
}
83+
// Ensure that we don't process the same commit multiple times
84+
const newCommits = commits.filter((commit) => !this.seen.has(commit.ref));
85+
const lastCommit = newCommits[newCommits.length - 1];
86+
if (!lastCommit) continue;
87+
this.version = lastCommit.version;
88+
newCommits.forEach((commit) => this.seen.add(commit.ref));
89+
90+
this.receiveCommits(newCommits);
8291
}
8392
}
8493
}
@@ -99,22 +108,39 @@ export class LongPollListener {
99108
) {
100109
this.headers = options.headers ?? {};
101110
this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
102-
this.getCommits = this.getCommits.bind(this);
103111
}
104112

105-
async getCommits(version: number) {
106-
const url = new URL(this.url);
107-
url.searchParams.append("version", version.toString());
113+
update(headers: HeadersInit) {
114+
this.headers = headers;
115+
}
116+
117+
async *listen(version: number, options: { signal?: AbortSignal } = {}) {
118+
while (!options?.signal || !options.signal.aborted) {
119+
const url = new URL(this.url);
120+
url.searchParams.append("version", version.toString());
108121

109-
const response = await this.fetch(url, {
110-
headers: this.headers,
111-
});
122+
try {
123+
const response = await this.fetch(url, {
124+
headers: this.headers,
125+
...(options?.signal && { signal: options.signal }),
126+
});
112127

113-
if (!response.ok) {
114-
throw new Error(`Failed to get commits. ${response.status}: ${response.statusText}`);
115-
}
128+
if (!response.ok) {
129+
throw new Error(`Failed to get commits. ${response.status}: ${response.statusText}`);
130+
}
131+
132+
const commitJSONs = (await response.json()) as CommitJSON[];
133+
yield commitJSONs;
134+
} catch (e) {
135+
console.error(e);
116136

117-
const commitJSONs = (await response.json()) as CommitJSON[];
118-
return commitJSONs;
137+
if (options.signal?.aborted) return;
138+
139+
// TODO: Implement a backoff strategy
140+
await new Promise<void>((resolve) => {
141+
setTimeout(() => resolve(), 3_000);
142+
});
143+
}
144+
}
119145
}
120146
}

packages/demo/src/editor/Editor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export function Editor({ doc }: Props) {
7979
body: JSON.stringify(commit.toJSON()),
8080
});
8181
},
82-
getCommits: listener.getCommits.bind(listener),
82+
listener,
8383
receiveCommits: (commits) => {
8484
setState((prev) =>
8585
commits.reduce((acc, commit) => acc.apply(receiveCommitTransaction(acc, commit)), prev),

0 commit comments

Comments
 (0)