Skip to content

[BREAKING/PROPOSAL] Generate unique string ids - #13

Merged
smoores-dev merged 4 commits into
handlewithcarecollective:mainfrom
Magic-Marker:feat/string-ids
Aug 28, 2025
Merged

[BREAKING/PROPOSAL] Generate unique string ids#13
smoores-dev merged 4 commits into
handlewithcarecollective:mainfrom
Magic-Marker:feat/string-ids

Conversation

@jevakallio

Copy link
Copy Markdown
Contributor

Hey @smoores-dev! 👋

We're integrating prosemirror-suggest-changes in our application, and we needed to add support for persistent, unique string ids for suggestions so that we can tie additional metadata (e.g. author information, conversation threads) to them.

I'm opening this draft PR as a conversation on whether you'd be open to considering something like this in upstream. The current iteration is a breaking change from your existing schema (it changes the id type from number to string), so we would probably want to have a backwards compatible approach here.

Let me know your thoughts -- this current patch works well for us now, but I'm happy to do the additional work to make this mergeable if there's broad interest.

Comment thread src/plugin.ts Outdated
generateId?: () => string;
}

function defaultGenerateId(): string {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This default function is pretty silly -- we don't use it ourselves, and I didn't want to add a dependency to crypto.randomUUID for this. Would probably need a better approach (or if we make this work with both number and string ids), could revert back to the old numbering id system.

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.

In react-prosemirror we also just use Math.random(): https://github.com/handlewithcarecollective/react-prosemirror/blob/main/src/plugins/reactKeys.ts#L5C15-L5C71. I'd be fine with using that same approach here, honestly — it's probably enough!

Comment thread src/plugin.ts Outdated
export const suggestChangesKey = new PluginKey<{ enabled: boolean }>(
export interface SuggestChangesPluginState {
enabled: boolean;
generateId: () => string;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We pass our id generator (@neuledge/id tagged id generator) here

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.

Rather than storing this in the state, since it's only ever used in the withSuggestChanges wrapper, maybe it should just be an optional argument to that function?

Comment thread src/plugin.ts Outdated
generateId?: () => string;
}

function defaultGenerateId(): string {

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.

In react-prosemirror we also just use Math.random(): https://github.com/handlewithcarecollective/react-prosemirror/blob/main/src/plugins/reactKeys.ts#L5C15-L5C71. I'd be fine with using that same approach here, honestly — it's probably enough!

Comment thread src/plugin.ts Outdated
export const suggestChangesKey = new PluginKey<{ enabled: boolean }>(
export interface SuggestChangesPluginState {
enabled: boolean;
generateId: () => string;

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.

Rather than storing this in the state, since it's only ever used in the withSuggestChanges wrapper, maybe it should just be an optional argument to that function?

Comment thread src/schema.ts Outdated
excludes: "insertion modification deletion",
attrs: {
id: { validate: "number" },
id: { validate: "string" },

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.

... hm. It does stink that this is a breaking schema change — there's not really any good way to migrate from the previous implementation to this one, if you have any documents persisted with these marks. I'll think about it a bit more, there may be nothing to do about it (this is a challenge with ProseMirror schemas more generally)

@smoores-dev

Copy link
Copy Markdown
Member

I think this is a reasonable proposal! Happy to chat about it more if you have thoughts — it would be great to think about whether it was possible to avoid a breaking change, but this is a pre-v1 project, so if we're going to need a breaking change anyway, now's probably the time!

@jevakallio

jevakallio commented Jun 30, 2025

Copy link
Copy Markdown
Contributor Author

I think this is a reasonable proposal! Happy to chat about it more if you have thoughts — it would be great to think about whether it was possible to avoid a breaking change

@smoores-dev I think the Attribute spec can define the validator as a function, so we could make it accept strings or numbers.
https://prosemirror.net/docs/ref/#model.AttributeSpec.validate.

The type of the id could be either string | number, or unknown if we want to keep it opaque.

I haven't tried this, but in theory it should work just fine.

New values would still be a breaking change for old schemas, but new schemas would support old ids and new ids. The backwards compatibility can be handled in userland by doing a staged migration -- upgrading the library first, and changing the generateId function once old clients have all had a chance to refresh.

Happy to make these changes if that sounds acceptable. In our application case we don't need this backwards compatibility, so I won't add the complexity to our fork unless you're happy to upstream it.

@smoores-dev

Copy link
Copy Markdown
Member

Right, good call! That sounds great to me — if you're open to making those changes, I would be happy to accept them!

@smoores-dev

Copy link
Copy Markdown
Member

@jevakallio are you still interested in taking this over the finish line?

@jevakallio

Copy link
Copy Markdown
Contributor Author

Yeah, absolutely. I have had to pivot my energies to other projects in the last month, but I'd love to be able to upstream this change (and other improvements we have made to the library)

Let me try to find some time to round up these changes and open PRs.

Apologies for leaving you hanging on this -- will aim to devote more energy towards it in the coming week/weeks.

@smoores-dev

Copy link
Copy Markdown
Member

Sounds great, I really appreciate it!

@VV-EE
VV-EE force-pushed the feat/string-ids branch 2 times, most recently from 1086e65 to d00c99c Compare July 22, 2025 16:11
@VV-EE

VV-EE commented Jul 22, 2025

Copy link
Copy Markdown
Contributor

Hey @smoores-dev I made the changes ( @jevakallio is away right now ), this is now backwards compatible. Now the id type is string | number. Also added a new test for this use case and for the original behaviour too. This looks good & mergeable to me, but if you find any issues I'll fix it.

We'll have quite a few fixes & refactors coming soon, I'll open PRs!

@jevakallio
jevakallio marked this pull request as ready for review July 28, 2025 13:55

@smoores-dev smoores-dev left a comment

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.

This looks awesome. One last clarification question and then I think this is good to go!

Comment thread src/withSuggestChanges.ts Outdated
Comment on lines +147 to +133
if (generateId && typeof suggestionId !== "number") {
suggestionId = generateId();

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.

Doesn't this mean that if the user-provided generateId returns a number, we won't call generateId to produce the next id? I'm not sure why just checking for the existence of generateId is insufficient here

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yeah, this part is a bit ugly. I think I did it this way because I wanted to exhaust the types. The undocumented assumption is that generateId will return string. But! The return type of generateId is string | number because I didn't want to complicate the typing.

It's hard to type "suggestionId is number if generateId is not defined, otherwise string ".

So this is an ugly but valid solution to a typing issue. I didn't spend that much time / brain on it.

In other words: the types of generateId and suggestionId is connected, that expression should always be true if either part of it is true, I've just added an extra guard around that. Yes, the typeof check can be removed if you want.

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.

Oh... ok. I guess I see. It seems like we should probably enforce that generateId returns a string, at least in the client-facing types, because if someone tries to use, say, a random number, they're probably gonna have a bad time haha. I can do that

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks! Agree.

@smoores-dev
smoores-dev merged commit b62ed45 into handlewithcarecollective:main Aug 28, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants