Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion packages/collab-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
"access": "public",
"exports": "./dist/index.js"
},
"scripts": {
"gen:ref": "typedoc"
},
"dependencies": {
"@stepwisehq/prosemirror-collab-commit": "^1.0.0"
},
Expand All @@ -15,7 +18,11 @@
"@typescript/native-preview": "7.0.0-dev.20260421.2",
"prosemirror-model": "^1.25.3",
"prosemirror-state": "^1.4.3",
"prosemirror-transform": "^1.10.4"
"prosemirror-transform": "^1.10.4",
"typedoc": "^0.28.19",
"typedoc-plugin-frontmatter": "^1.3.1",
"typedoc-plugin-markdown": "^4.11.0",
"typescript": "^6.0.3"
},
"peerDependencies": {
"prosemirror-model": "^1.0.0",
Expand Down
91 changes: 85 additions & 6 deletions packages/collab-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ import {
} from "@stepwisehq/prosemirror-collab-commit/collab-commit";
import { EditorState } from "prosemirror-state";

export { receiveCommitTransaction, getVersion, Commit, type CommitJSON, type NodeJSON };
export {
receiveCommitTransaction,
getVersion,
Commit,
type CommitJSON,
type NodeJSON,
};

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

Expand All @@ -20,11 +26,47 @@ export interface CommitsListener {
}

export interface CollabClientConfig {
/**
* Sends local commits to a remote server to be merged into the remote document state.
* The endpoint this function hits is defined by you, and should call the
* CollabAuthority's {@link https://pitter-patter.dev/docs/collab/reference/collab-server/classes/CollabAuthority#receivecommit | receiveCommit}
* function.
*
* @param commit - the latest prosemirror commit made by the local user
*/
sendCommit: (commit: Commit) => Promise<void>;
/**
* A listener for remote commits.
*
* Currently the only provided option is the {@link LongPollListener}.
Comment thread
pdiffley marked this conversation as resolved.
Outdated
*
* Support for realtime databases like Firestore and Convex is planned
* and can be expedited on request. Contact hello@handlewithcare.dev to inquire.
Comment thread
pdiffley marked this conversation as resolved.
Outdated
*/
listener: CommitsListener;
// Todo: The example in this doc rely's on some react context, how to show it otherwise
// It feels useful to show that you use receiveCommitTransaction to merge the editor
// state, but that might just be because I wouldn't know how to do it otherwise.
// See the doc in the Presence client config's receiveIndicators for an alternative approach.
Comment on lines +38 to +41

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove this TODO now yeah?

/**
* Receives an array of commits and merges them into your local editor state.
*
* @example
* ```
* import receiveCommitTransaction from "@stepwisehq/prosemirror-collab-commit/collab-commit";
*
* receiveIndicators: (indicators) => {
* setState((prev) => prev.apply(receivePresenceTransaction(prev, indicators)));
* },
Comment thread
pdiffley marked this conversation as resolved.
Outdated
* ```
*/
receiveCommits: (commits: Commit[]) => void;
}

/**
* The client that manages sending local editor state changes to the remote server and merging
* remote changes into local editor state.
*/
export class CollabClient {
private sending: null | string = null;

Expand All @@ -38,6 +80,9 @@ export class CollabClient {
this.listener = config.listener;
}

/**
* Send local editor state changes to the remote server.
*/
async send(editorState: EditorState) {
const commit = sendableCommit(editorState);
if (!commit) return;
Expand All @@ -57,11 +102,18 @@ export class CollabClient {
}
}

/**
* Updates the desired portion of the client's `CollabClientConfig`. For example, this can
* be used to update the auth headers used by `sendCommit`.
*/
update(config: Partial<Omit<CollabClientConfig, "listener">>) {
if (config.sendCommit) this.sendCommit = config.sendCommit;
if (config.receiveCommits) this.receiveCommits = config.receiveCommits;
}

/**
* Have the client start listening for remote commits. This function should only be called once.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Have the client start listening for remote commits. This function should only be called once.
* Start listening for remote commits.
*
* This function should only be called once.

*/
async listen(editorState: EditorState, signal?: AbortSignal) {
for await (const newCommits of this.listener.listen(editorState, {
signal,
Expand All @@ -73,15 +125,30 @@ export class CollabClient {
}

export interface LongPollListenerOptions {
timeout?: number;
// Todo: the timeout option is not currently used in the LongPollListner. Add support for it.
// timeout?: number;
/**
* Any headers that need to be included in requests to your long polling endpoint. Defaults to an empty object.
*/
headers?: HeadersInit;
/**
* The fetch method to use when making requests. Defaults to the global fetch method.
*/
fetch?: typeof globalThis.fetch;
}

/**
* A CommitsListener that polls an endpoint for remote updates to a document. Intended to be used
* with an remote long polling endpoint that calls a Collab Authority's {@link https://pitter-patter.dev/docs/collab/reference/collab-server/classes/CollabAuthority#listenforcommit | listenForCommit}
* function to efficiently listen for updates.
*/
export class LongPollListener {
private headers: HeadersInit;
private fetch: typeof globalThis.fetch;

/**
* @param url - the url that polling requests will be sent to
*/
constructor(
private url: URL,
options: LongPollListenerOptions = {},
Expand All @@ -90,15 +157,23 @@ export class LongPollListener {
this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
}

/**
* Update the headers sent with long polling requests.
*/
update(headers: HeadersInit) {
this.headers = headers;
}

async *listen(editorState: EditorState, options: { signal?: AbortSignal | undefined } = {}) {
async *listen(
editorState: EditorState,
options: { signal?: AbortSignal | undefined } = {},
) {
const seen = new Set<string>();
let version = getVersion(editorState);
if (version === undefined) {
throw new Error("EditorState is missing the collab plugin, unable to listen for changes");
throw new Error(
"EditorState is missing the collab plugin, unable to listen for changes",
);
}

while (!options?.signal || !options.signal.aborted) {
Expand All @@ -112,12 +187,16 @@ export class LongPollListener {
});

if (!response.ok) {
throw new Error(`Failed to get commits. ${response.status}: ${response.statusText}`);
throw new Error(
`Failed to get commits. ${response.status}: ${response.statusText}`,
);
}

const commitJSONs = (await response.json()) as CommitJSON[];

const commits = commitJSONs.map((json) => Commit.FromJSON(editorState.schema, json));
const commits = commitJSONs.map((json) =>
Commit.FromJSON(editorState.schema, json),
);

// Ensure that we don't process the same commit multiple times
const newCommits = commits.filter((commit) => !seen.has(commit.ref));
Expand Down
24 changes: 24 additions & 0 deletions packages/collab-client/typedoc-plugin-frontmatter.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { MarkdownPageEvent } from "typedoc-plugin-markdown";

/**
* @param {import('typedoc-plugin-markdown').MarkdownApplication} app
*/
export function load(app) {
app.renderer.on(
MarkdownPageEvent.BEGIN,
/** @param {import('typedoc-plugin-markdown').MarkdownPageEvent} page */
(page) => {
page.frontmatter = {
title: page.model?.name,
};
},
);

app.renderer.on(
MarkdownPageEvent.END,
/** @param {import('typedoc-plugin-markdown').MarkdownPageEvent} page */
(page) => {
page.contents = page.contents.replace(/(\[.*?\]\(.*?)\.md(\))/g, "$1$2");
},
);
}
20 changes: 20 additions & 0 deletions packages/collab-client/typedoc.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/** @type {import('typedoc').TypeDocOptions & import('typedoc-plugin-markdown').PluginOptions} */
const config = {
entryPoints: ["./src/index.ts"],
plugin: [
"typedoc-plugin-markdown",
"typedoc-plugin-frontmatter",
"./typedoc-plugin-frontmatter.mjs",
],
out: "../docs/content/docs/collab/reference/collab-client",
readme: "none",
cleanOutputDir: true,
hideBreadcrumbs: true,
hidePageHeader: true,
useCodeBlocks: true,
expandObjects: true,
expandParameters: true,
publicPath: "/docs/collab/reference/collab-client",
};

export default config;
9 changes: 8 additions & 1 deletion packages/collab-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
"access": "public",
"exports": "./dist/index.js"
},
"scripts": {
"gen:ref": "typedoc"
},
"dependencies": {
"@stepwisehq/prosemirror-collab-commit": "^1.0.3",
"redis": "^5.8.2"
Expand All @@ -16,7 +19,11 @@
"@typescript/native-preview": "7.0.0-dev.20260421.2",
"prosemirror-model": "^1.25.3",
"prosemirror-state": "^1.4.3",
"prosemirror-transform": "^1.10.4"
"prosemirror-transform": "^1.10.4",
"typedoc": "^0.28.19",
"typedoc-plugin-frontmatter": "^1.3.1",
"typedoc-plugin-markdown": "^4.11.0",
"typescript": "^6.0.3"
},
"peerDependencies": {
"prosemirror-model": "^1.0.0",
Expand Down
Loading
Loading