@mp-lb/mdkit 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/README.md +8 -2
  2. package/dist/collaboration/useMdKitCollaboration.d.ts +5 -0
  3. package/dist/collaboration/useMdKitCollaboration.d.ts.map +1 -0
  4. package/dist/collaboration/useMdKitCollaboration.js +4 -0
  5. package/dist/core/checkpointPolicy.d.ts +10 -0
  6. package/dist/core/checkpointPolicy.d.ts.map +1 -0
  7. package/dist/core/checkpointPolicy.js +9 -0
  8. package/dist/core/documentEngine.d.ts +1 -0
  9. package/dist/core/documentEngine.d.ts.map +1 -0
  10. package/dist/core/index.d.ts +1 -0
  11. package/dist/core/index.d.ts.map +1 -0
  12. package/dist/document/MdKitConflictPanel.d.ts +5 -0
  13. package/dist/document/MdKitConflictPanel.d.ts.map +1 -0
  14. package/dist/document/MdKitConflictPanel.js +4 -0
  15. package/dist/document/MdKitDocumentToolbar.d.ts +6 -0
  16. package/dist/document/MdKitDocumentToolbar.d.ts.map +1 -0
  17. package/dist/document/MdKitDocumentToolbar.js +5 -0
  18. package/dist/document/documentTypes.d.ts +6 -0
  19. package/dist/document/documentTypes.d.ts.map +1 -0
  20. package/dist/document/useMdKitDocument.d.ts +5 -0
  21. package/dist/document/useMdKitDocument.d.ts.map +1 -0
  22. package/dist/document/useMdKitDocument.js +4 -0
  23. package/dist/fastify.d.ts +1 -0
  24. package/dist/fastify.d.ts.map +1 -0
  25. package/dist/index.d.ts +4 -1
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/markdown/MarkdownBubbleMenu.d.ts +1 -0
  28. package/dist/markdown/MarkdownBubbleMenu.d.ts.map +1 -0
  29. package/dist/markdown/MarkdownPasteExtension.d.ts +1 -0
  30. package/dist/markdown/MarkdownPasteExtension.d.ts.map +1 -0
  31. package/dist/markdown/MarkdownSearchExtension.d.ts +1 -0
  32. package/dist/markdown/MarkdownSearchExtension.d.ts.map +1 -0
  33. package/dist/markdown/MarkdownSearchPanel.d.ts +1 -0
  34. package/dist/markdown/MarkdownSearchPanel.d.ts.map +1 -0
  35. package/dist/markdown/MdKitEditor.d.ts +11 -0
  36. package/dist/markdown/MdKitEditor.d.ts.map +1 -0
  37. package/dist/markdown/MdKitEditor.js +10 -2
  38. package/dist/markdown/MdKitView.d.ts +9 -1
  39. package/dist/markdown/MdKitView.d.ts.map +1 -0
  40. package/dist/markdown/MdKitView.js +7 -2
  41. package/dist/markdown/TiptapMarkdownSurface.d.ts +1 -0
  42. package/dist/markdown/TiptapMarkdownSurface.d.ts.map +1 -0
  43. package/dist/markdown/TiptapMarkdownSurface.js +3 -22
  44. package/dist/markdown/createMdKitTiptapExtensions.d.ts +1 -0
  45. package/dist/markdown/createMdKitTiptapExtensions.d.ts.map +1 -0
  46. package/dist/markdown/editorDebug.d.ts +1 -0
  47. package/dist/markdown/editorDebug.d.ts.map +1 -0
  48. package/dist/markdown/markdownFenceRanges.d.ts +1 -0
  49. package/dist/markdown/markdownFenceRanges.d.ts.map +1 -0
  50. package/dist/markdown/normalizeMarkdownSerialization.d.ts +1 -0
  51. package/dist/markdown/normalizeMarkdownSerialization.d.ts.map +1 -0
  52. package/dist/markdown/prepareMarkdownForEditorHydration.d.ts +1 -0
  53. package/dist/markdown/prepareMarkdownForEditorHydration.d.ts.map +1 -0
  54. package/dist/markdown/preserveMarkdownWhitespace.d.ts +1 -0
  55. package/dist/markdown/preserveMarkdownWhitespace.d.ts.map +1 -0
  56. package/dist/markdown/yamlFrontMatter.d.ts +1 -0
  57. package/dist/markdown/yamlFrontMatter.d.ts.map +1 -0
  58. package/dist/server.d.ts +1 -0
  59. package/dist/server.d.ts.map +1 -0
  60. package/dist/theme/MdKitThemeEditor.d.ts +5 -0
  61. package/dist/theme/MdKitThemeEditor.d.ts.map +1 -0
  62. package/dist/theme/MdKitThemeEditor.js +4 -0
  63. package/dist/theme/editorTheme.d.ts +1 -0
  64. package/dist/theme/editorTheme.d.ts.map +1 -0
  65. package/dist/theme/editorTheme.js +8 -8
  66. package/dist/transport/backend.d.ts +13 -0
  67. package/dist/transport/backend.d.ts.map +1 -0
  68. package/dist/transport/backend.js +6 -0
  69. package/dist/transport/fastify.d.ts +5 -0
  70. package/dist/transport/fastify.d.ts.map +1 -0
  71. package/dist/transport/fastify.js +4 -0
  72. package/dist/transport/http.d.ts +1 -0
  73. package/dist/transport/http.d.ts.map +1 -0
  74. package/dist/transport/index.d.ts +1 -0
  75. package/dist/transport/index.d.ts.map +1 -0
  76. package/dist/transport/rest.d.ts +6 -0
  77. package/dist/transport/rest.d.ts.map +1 -0
  78. package/dist/transport/rest.js +5 -0
  79. package/dist/transport/store.d.ts +1 -0
  80. package/dist/transport/store.d.ts.map +1 -0
  81. package/dist/transport/trpcClient.d.ts +8 -0
  82. package/dist/transport/trpcClient.d.ts.map +1 -0
  83. package/dist/transport/trpcClient.js +7 -0
  84. package/dist/transport/trpcServer.d.ts +6 -0
  85. package/dist/transport/trpcServer.d.ts.map +1 -0
  86. package/dist/transport/trpcServer.js +5 -0
  87. package/dist/trpc/client.d.ts +1 -0
  88. package/dist/trpc/client.d.ts.map +1 -0
  89. package/dist/trpc/server.d.ts +1 -0
  90. package/dist/trpc/server.d.ts.map +1 -0
  91. package/dist/trpc.d.ts +1 -0
  92. package/dist/trpc.d.ts.map +1 -0
  93. package/dist/ui/joinClassNames.d.ts +1 -0
  94. package/dist/ui/joinClassNames.d.ts.map +1 -0
  95. package/dist/versioning/VersionHistoryPanel.d.ts +5 -0
  96. package/dist/versioning/VersionHistoryPanel.d.ts.map +1 -0
  97. package/dist/versioning/VersionHistoryPanel.js +4 -0
  98. package/dist/versioning/useMdKitDocumentVersions.d.ts +5 -0
  99. package/dist/versioning/useMdKitDocumentVersions.d.ts.map +1 -0
  100. package/dist/versioning/useMdKitDocumentVersions.js +4 -0
  101. package/dist/yjs/MdKitMarkdownYjs.d.ts +1 -0
  102. package/dist/yjs/MdKitMarkdownYjs.d.ts.map +1 -0
  103. package/dist/yjs/index.d.ts +1 -0
  104. package/dist/yjs/index.d.ts.map +1 -0
  105. package/package.json +10 -12
  106. package/src/collaboration/useMdKitCollaboration.ts +528 -0
  107. package/src/core/checkpointPolicy.ts +107 -0
  108. package/src/core/documentEngine.ts +175 -0
  109. package/src/core/index.ts +33 -0
  110. package/src/document/MdKitConflictPanel.tsx +129 -0
  111. package/src/document/MdKitDocumentToolbar.tsx +141 -0
  112. package/src/document/documentTypes.ts +89 -0
  113. package/src/document/useMdKitDocument.ts +543 -0
  114. package/src/fastify.ts +6 -0
  115. package/src/index.ts +89 -0
  116. package/src/markdown/MarkdownBubbleMenu.tsx +271 -0
  117. package/src/markdown/MarkdownPasteExtension.ts +81 -0
  118. package/src/markdown/MarkdownSearchExtension.ts +77 -0
  119. package/src/markdown/MarkdownSearchPanel.tsx +98 -0
  120. package/src/markdown/MdKitEditor.tsx +75 -0
  121. package/src/markdown/MdKitView.tsx +80 -0
  122. package/src/markdown/TiptapMarkdownSurface.tsx +923 -0
  123. package/src/markdown/createMdKitTiptapExtensions.ts +42 -0
  124. package/src/markdown/editorDebug.ts +5 -0
  125. package/src/markdown/markdownFenceRanges.ts +68 -0
  126. package/src/markdown/normalizeMarkdownSerialization.ts +55 -0
  127. package/src/markdown/prepareMarkdownForEditorHydration.ts +23 -0
  128. package/src/markdown/preserveMarkdownWhitespace.ts +143 -0
  129. package/src/markdown/yamlFrontMatter.ts +135 -0
  130. package/src/server.ts +6 -0
  131. package/src/styles.css +125 -53
  132. package/src/theme/MdKitThemeEditor.tsx +134 -0
  133. package/src/theme/editorTheme.ts +72 -0
  134. package/src/transport/backend.ts +220 -0
  135. package/src/transport/fastify.ts +57 -0
  136. package/src/transport/http.ts +126 -0
  137. package/src/transport/index.ts +12 -0
  138. package/src/transport/rest.ts +80 -0
  139. package/src/transport/store.ts +45 -0
  140. package/src/transport/trpcClient.ts +90 -0
  141. package/src/transport/trpcServer.ts +66 -0
  142. package/src/trpc/client.ts +11 -0
  143. package/src/trpc/server.ts +12 -0
  144. package/src/trpc.ts +11 -0
  145. package/src/ui/joinClassNames.ts +3 -0
  146. package/src/versioning/VersionHistoryPanel.tsx +146 -0
  147. package/src/versioning/useMdKitDocumentVersions.ts +146 -0
  148. package/src/yjs/MdKitMarkdownYjs.ts +111 -0
  149. package/src/yjs/index.ts +8 -0
  150. package/docs/.vitepress/config.ts +0 -47
  151. package/docs/api.md +0 -512
  152. package/docs/architecture.md +0 -96
  153. package/docs/collaboration-persistence.md +0 -147
  154. package/docs/index.md +0 -341
  155. package/docs/permissions.md +0 -139
  156. package/docs/plain-text.md +0 -131
  157. package/docs/rest.md +0 -98
  158. package/docs/shadcn.md +0 -125
  159. package/docs/styling.md +0 -373
  160. package/docs/use-cases.md +0 -148
@@ -0,0 +1,528 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { HocuspocusProvider } from "@hocuspocus/provider";
3
+ import * as Y from "yjs";
4
+ import type {
5
+ MdKitCollaborationParticipant,
6
+ MdKitCollaborationPresence,
7
+ MdKitCollaborationSession,
8
+ MdKitCollaborationStatus,
9
+ } from "../document/documentTypes";
10
+
11
+ export type UseMdKitCollaborationOptions = {
12
+ collaborator: MdKitCollaborationParticipant;
13
+ documentId: string | null;
14
+ enabled?: boolean;
15
+ endpoint: string | null;
16
+ getToken?: () => Promise<string | null>;
17
+ resolveRoomName?: (documentId: string) => string;
18
+ };
19
+
20
+ type MdKitCollaborationDebugGlobal = typeof globalThis & {
21
+ __MDKIT_COLLAB_DEBUG__?: boolean;
22
+ };
23
+
24
+ const isCollaborationDebugEnabled = () => {
25
+ if (
26
+ (globalThis as MdKitCollaborationDebugGlobal).__MDKIT_COLLAB_DEBUG__ ===
27
+ true
28
+ ) {
29
+ return true;
30
+ }
31
+
32
+ if (typeof window === "undefined") {
33
+ return false;
34
+ }
35
+
36
+ try {
37
+ return window.localStorage.getItem("mdkit:collab-debug") === "true";
38
+ } catch {
39
+ return false;
40
+ }
41
+ };
42
+
43
+ const stringifyDebugDetails = (details: Record<string, unknown>): string => {
44
+ const seen = new WeakSet<object>();
45
+
46
+ try {
47
+ return JSON.stringify(details, (_key, value: unknown) => {
48
+ if (typeof value === "bigint") {
49
+ return value.toString();
50
+ }
51
+
52
+ if (!value || typeof value !== "object") {
53
+ return value;
54
+ }
55
+
56
+ if (seen.has(value)) {
57
+ return "[Circular]";
58
+ }
59
+
60
+ seen.add(value);
61
+ return value;
62
+ });
63
+ } catch (error) {
64
+ return JSON.stringify({
65
+ serializationError:
66
+ error instanceof Error ? error.message : "Unable to serialize details",
67
+ });
68
+ }
69
+ };
70
+
71
+ const debugCollaboration = (event: string, details: Record<string, unknown>) => {
72
+ if (!isCollaborationDebugEnabled()) {
73
+ return;
74
+ }
75
+
76
+ console.info(
77
+ `MDKIT_COLLAB_DEBUG ${event} ${stringifyDebugDetails(details)}`,
78
+ );
79
+ };
80
+
81
+ const createColorFromId = (id: string): string => {
82
+ let hash = 0;
83
+
84
+ for (let index = 0; index < id.length; index += 1) {
85
+ hash = (hash << 5) - hash + id.charCodeAt(index);
86
+ hash |= 0;
87
+ }
88
+
89
+ return `hsl(${Math.abs(hash) % 360}, 85%, 55%)`;
90
+ };
91
+
92
+ const parsePresence = (
93
+ clientId: number,
94
+ state: unknown,
95
+ localClientId: number,
96
+ ): MdKitCollaborationPresence | null => {
97
+ if (!state || typeof state !== "object") {
98
+ return null;
99
+ }
100
+
101
+ const user = "user" in state ? (state as { user?: unknown }).user : state;
102
+
103
+ if (!user || typeof user !== "object") {
104
+ return null;
105
+ }
106
+
107
+ const { color, id, imageUrl, name } = user as {
108
+ color?: unknown;
109
+ id?: unknown;
110
+ imageUrl?: unknown;
111
+ name?: unknown;
112
+ };
113
+
114
+ if (typeof name !== "string") {
115
+ return null;
116
+ }
117
+
118
+ return {
119
+ clientId,
120
+ color:
121
+ typeof color === "string"
122
+ ? color
123
+ : createColorFromId(typeof id === "string" ? id : String(clientId)),
124
+ id: typeof id === "string" ? id : String(clientId),
125
+ imageUrl: typeof imageUrl === "string" ? imageUrl : undefined,
126
+ isLocal: clientId === localClientId,
127
+ name,
128
+ };
129
+ };
130
+
131
+ const summarizeAwarenessState = (clientId: number, state: unknown) => {
132
+ if (!state || typeof state !== "object") {
133
+ return {
134
+ clientId,
135
+ hasState: false,
136
+ };
137
+ }
138
+
139
+ const user = "user" in state ? (state as { user?: unknown }).user : state;
140
+
141
+ return {
142
+ clientId,
143
+ hasCursor: "cursor" in state,
144
+ hasState: true,
145
+ hasUser: !!user,
146
+ user,
147
+ };
148
+ };
149
+
150
+ const summarizeParticipants = (
151
+ participants: MdKitCollaborationPresence[],
152
+ ) =>
153
+ participants.map((participant) => ({
154
+ clientId: participant.clientId,
155
+ id: participant.id,
156
+ isLocal: participant.isLocal,
157
+ name: participant.name,
158
+ }));
159
+
160
+ const areParticipantsEqual = (
161
+ left: MdKitCollaborationPresence[],
162
+ right: MdKitCollaborationPresence[],
163
+ ) =>
164
+ left.length === right.length &&
165
+ left.every((leftParticipant, index) => {
166
+ const rightParticipant = right[index];
167
+
168
+ return (
169
+ rightParticipant &&
170
+ leftParticipant.clientId === rightParticipant.clientId &&
171
+ leftParticipant.color === rightParticipant.color &&
172
+ leftParticipant.id === rightParticipant.id &&
173
+ leftParticipant.imageUrl === rightParticipant.imageUrl &&
174
+ leftParticipant.isLocal === rightParticipant.isLocal &&
175
+ leftParticipant.name === rightParticipant.name
176
+ );
177
+ });
178
+
179
+ const setProviderUser = (
180
+ provider: HocuspocusProvider,
181
+ collaborator: MdKitCollaborationParticipant,
182
+ ) => {
183
+ provider.setAwarenessField("user", {
184
+ color: collaborator.color,
185
+ id: collaborator.id,
186
+ imageUrl: collaborator.imageUrl || undefined,
187
+ name: collaborator.name,
188
+ });
189
+ };
190
+
191
+ /**
192
+ * Creates a Hocuspocus/Yjs collaboration session for {@link MdKitEditor},
193
+ * managing the provider connection, local participant, and presence.
194
+ */
195
+ export const useMdKitCollaboration = (
196
+ options: UseMdKitCollaborationOptions,
197
+ ): MdKitCollaborationSession | null => {
198
+ const {
199
+ collaborator,
200
+ documentId,
201
+ enabled = true,
202
+ endpoint,
203
+ getToken,
204
+ resolveRoomName,
205
+ } = options;
206
+
207
+ const [status, setStatus] =
208
+ useState<MdKitCollaborationStatus>("disconnected");
209
+ const [participants, setParticipants] = useState<
210
+ MdKitCollaborationPresence[]
211
+ >([]);
212
+ const [provider, setProvider] = useState<HocuspocusProvider | null>(null);
213
+
214
+ const normalizedCollaborator = useMemo(
215
+ () => ({
216
+ ...collaborator,
217
+ color: collaborator.color || createColorFromId(collaborator.id),
218
+ }),
219
+ [
220
+ collaborator.color,
221
+ collaborator.id,
222
+ collaborator.imageUrl,
223
+ collaborator.name,
224
+ ],
225
+ );
226
+
227
+ const roomName = useMemo(() => {
228
+ if (!documentId) {
229
+ return "";
230
+ }
231
+
232
+ return resolveRoomName ? resolveRoomName(documentId) : documentId;
233
+ }, [documentId, resolveRoomName]);
234
+
235
+ const ydoc = useMemo(() => {
236
+ void roomName;
237
+ return new Y.Doc();
238
+ }, [roomName]);
239
+
240
+ const setNextParticipants = useCallback(
241
+ (nextParticipants: MdKitCollaborationPresence[], source: string) => {
242
+ setParticipants((currentParticipants) =>
243
+ areParticipantsEqual(currentParticipants, nextParticipants)
244
+ ? currentParticipants
245
+ : (() => {
246
+ debugCollaboration("participants_update", {
247
+ nextParticipants: summarizeParticipants(nextParticipants),
248
+ previousParticipants:
249
+ summarizeParticipants(currentParticipants),
250
+ roomName,
251
+ source,
252
+ });
253
+
254
+ return nextParticipants;
255
+ })(),
256
+ );
257
+ },
258
+ [roomName],
259
+ );
260
+
261
+ const updateParticipantsFromProvider = useCallback(
262
+ (nextProvider: HocuspocusProvider | null, source: string) => {
263
+ const awareness = nextProvider?.awareness;
264
+
265
+ if (!awareness) {
266
+ debugCollaboration("awareness_missing", {
267
+ roomName,
268
+ source,
269
+ });
270
+ setNextParticipants([], source);
271
+ return;
272
+ }
273
+
274
+ const rawStates = Array.from(awareness.getStates());
275
+ const nextParticipants = rawStates
276
+ .map(([clientId, state]) =>
277
+ parsePresence(clientId, state, ydoc.clientID),
278
+ )
279
+ .filter((presence): presence is MdKitCollaborationPresence =>
280
+ Boolean(presence),
281
+ );
282
+
283
+ debugCollaboration("awareness_map_read", {
284
+ localClientId: ydoc.clientID,
285
+ parsedParticipants: summarizeParticipants(nextParticipants),
286
+ rawStates: rawStates.map(([clientId, state]) =>
287
+ summarizeAwarenessState(clientId, state),
288
+ ),
289
+ roomName,
290
+ source,
291
+ });
292
+
293
+ setNextParticipants(nextParticipants, source);
294
+ },
295
+ [roomName, setNextParticipants, ydoc.clientID],
296
+ );
297
+
298
+ useEffect(() => {
299
+ if (!enabled || !documentId || !endpoint) {
300
+ debugCollaboration("provider_skipped", {
301
+ documentId,
302
+ enabled,
303
+ endpoint,
304
+ roomName,
305
+ });
306
+
307
+ setProvider(null);
308
+ setStatus("disconnected");
309
+ setNextParticipants([], "provider_skipped");
310
+ return;
311
+ }
312
+
313
+ debugCollaboration("provider_create", {
314
+ collaborator: normalizedCollaborator,
315
+ documentId,
316
+ endpoint,
317
+ localClientId: ydoc.clientID,
318
+ roomName,
319
+ });
320
+
321
+ let nextProvider: HocuspocusProvider | null = null;
322
+
323
+ nextProvider = new HocuspocusProvider({
324
+ document: ydoc,
325
+ name: roomName,
326
+ onConnect: () => {
327
+ if (nextProvider) {
328
+ setProviderUser(nextProvider, normalizedCollaborator);
329
+ }
330
+
331
+ debugCollaboration("provider_connect", {
332
+ localClientId: ydoc.clientID,
333
+ roomName,
334
+ });
335
+ setStatus("connected");
336
+
337
+ globalThis.setTimeout(() => {
338
+ updateParticipantsFromProvider(nextProvider, "provider_connect");
339
+ }, 0);
340
+ },
341
+ onDisconnect: () => {
342
+ debugCollaboration("provider_disconnect", {
343
+ localClientId: ydoc.clientID,
344
+ roomName,
345
+ });
346
+ setStatus("disconnected");
347
+ },
348
+ onStatus: ({ status: nextStatus }) => {
349
+ debugCollaboration("provider_status", {
350
+ localClientId: ydoc.clientID,
351
+ roomName,
352
+ status: nextStatus,
353
+ });
354
+
355
+ if (nextStatus === "connecting") {
356
+ setStatus("connecting");
357
+ }
358
+ },
359
+ onAwarenessChange: ({ states }) => {
360
+ debugCollaboration("provider_awareness_change", {
361
+ localClientId: ydoc.clientID,
362
+ rawStates: states,
363
+ roomName,
364
+ });
365
+
366
+ updateParticipantsFromProvider(
367
+ nextProvider,
368
+ "provider_awareness_change",
369
+ );
370
+ },
371
+ token: async () => {
372
+ const token = getToken ? await getToken() : null;
373
+ debugCollaboration("provider_token", {
374
+ hasToken: !!token,
375
+ roomName,
376
+ source: getToken ? "custom" : "default",
377
+ });
378
+ return token || "notoken";
379
+ },
380
+ url: endpoint,
381
+ });
382
+
383
+ setProvider(nextProvider);
384
+ setStatus("connecting");
385
+
386
+ return () => {
387
+ nextProvider?.destroy();
388
+ nextProvider = null;
389
+ setProvider(null);
390
+ setStatus("disconnected");
391
+ setNextParticipants([], "provider_destroy");
392
+ };
393
+ }, [
394
+ documentId,
395
+ enabled,
396
+ endpoint,
397
+ getToken,
398
+ normalizedCollaborator,
399
+ roomName,
400
+ setNextParticipants,
401
+ updateParticipantsFromProvider,
402
+ ydoc,
403
+ ]);
404
+
405
+ useEffect(() => {
406
+ debugCollaboration("session_options", {
407
+ collaborator: normalizedCollaborator,
408
+ documentId,
409
+ enabled,
410
+ endpoint,
411
+ localClientId: ydoc.clientID,
412
+ roomName,
413
+ });
414
+ }, [
415
+ documentId,
416
+ enabled,
417
+ endpoint,
418
+ normalizedCollaborator,
419
+ roomName,
420
+ ydoc.clientID,
421
+ ]);
422
+
423
+ useEffect(() => {
424
+ debugCollaboration("session_state", {
425
+ isCollaborating: participants.some((participant) => !participant.isLocal),
426
+ participants: summarizeParticipants(participants),
427
+ roomName,
428
+ status: provider ? status : "disconnected",
429
+ });
430
+ }, [participants, provider, roomName, status]);
431
+
432
+ useEffect(() => {
433
+ if (!provider) {
434
+ return;
435
+ }
436
+
437
+ debugCollaboration("awareness_set_user", {
438
+ localClientId: ydoc.clientID,
439
+ roomName,
440
+ user: {
441
+ color: normalizedCollaborator.color,
442
+ id: normalizedCollaborator.id,
443
+ imageUrl: normalizedCollaborator.imageUrl || undefined,
444
+ name: normalizedCollaborator.name,
445
+ },
446
+ });
447
+
448
+ setProviderUser(provider, normalizedCollaborator);
449
+ updateParticipantsFromProvider(provider, "awareness_set_user");
450
+ }, [
451
+ normalizedCollaborator,
452
+ provider,
453
+ roomName,
454
+ updateParticipantsFromProvider,
455
+ ydoc.clientID,
456
+ ]);
457
+
458
+ useEffect(() => {
459
+ const awareness = provider?.awareness;
460
+
461
+ if (!awareness) {
462
+ debugCollaboration("awareness_effect_missing", {
463
+ roomName,
464
+ });
465
+ setNextParticipants([], "awareness_effect_missing");
466
+ return;
467
+ }
468
+
469
+ const handleAwarenessChange = () =>
470
+ updateParticipantsFromProvider(provider, "awareness_change_event");
471
+
472
+ awareness.on("change", handleAwarenessChange);
473
+ updateParticipantsFromProvider(provider, "awareness_change_subscribe");
474
+
475
+ return () => {
476
+ awareness.off("change", handleAwarenessChange);
477
+ };
478
+ }, [provider, roomName, setNextParticipants, updateParticipantsFromProvider]);
479
+
480
+ useEffect(() => {
481
+ if (!provider) {
482
+ return;
483
+ }
484
+
485
+ const handleProviderAwareness = () =>
486
+ updateParticipantsFromProvider(provider, "provider_awareness_event");
487
+
488
+ provider.on("awarenessChange", handleProviderAwareness);
489
+ provider.on("awarenessUpdate", handleProviderAwareness);
490
+ updateParticipantsFromProvider(provider, "provider_awareness_subscribe");
491
+
492
+ return () => {
493
+ provider.off("awarenessChange", handleProviderAwareness);
494
+ provider.off("awarenessUpdate", handleProviderAwareness);
495
+ };
496
+ }, [provider, updateParticipantsFromProvider]);
497
+
498
+ return useMemo(() => {
499
+ if (!enabled || !documentId || !endpoint) {
500
+ return null;
501
+ }
502
+
503
+ const otherParticipants = participants.filter(
504
+ (participant) => !participant.isLocal,
505
+ );
506
+
507
+ return {
508
+ collaborator: normalizedCollaborator,
509
+ document: ydoc,
510
+ isCollaborating: otherParticipants.length > 0,
511
+ otherParticipants,
512
+ participants,
513
+ provider,
514
+ roomName,
515
+ status: provider ? status : "disconnected",
516
+ };
517
+ }, [
518
+ documentId,
519
+ enabled,
520
+ endpoint,
521
+ normalizedCollaborator,
522
+ participants,
523
+ provider,
524
+ roomName,
525
+ status,
526
+ ydoc,
527
+ ]);
528
+ };
@@ -0,0 +1,107 @@
1
+ import type {
2
+ MdKitDocumentVersionDetail,
3
+ MdKitDocumentWriteInput,
4
+ MdKitDocumentWriteResult,
5
+ } from "../document/documentTypes";
6
+
7
+ export type MdKitCheckpointPolicyInput = {
8
+ currentContent: string;
9
+ documentId: string;
10
+ editDistance: number;
11
+ previousCheckpoint: MdKitDocumentVersionDetail | null;
12
+ previousCheckpointContent: string | null;
13
+ timeSinceLastCheckpointMs: number | null;
14
+ writeInput: MdKitDocumentWriteInput;
15
+ writeResult: MdKitDocumentWriteResult;
16
+ };
17
+
18
+ export type MdKitCheckpointPolicy = {
19
+ shouldCheckpoint(
20
+ input: MdKitCheckpointPolicyInput,
21
+ ): boolean | Promise<boolean>;
22
+ };
23
+
24
+ export type MdKitSmartCheckpointPolicyOptions = {
25
+ minEditDistance?: number;
26
+ minIntervalMs?: number;
27
+ };
28
+
29
+ const defaultMinEditDistance = 250;
30
+ const defaultMinIntervalMs = 5 * 60_000;
31
+
32
+ export const measureMdKitEditDistance = (left: string, right: string) => {
33
+ if (left === right) {
34
+ return 0;
35
+ }
36
+
37
+ if (left.length === 0) {
38
+ return right.length;
39
+ }
40
+
41
+ if (right.length === 0) {
42
+ return left.length;
43
+ }
44
+
45
+ let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
46
+ let current = new Array<number>(right.length + 1);
47
+
48
+ for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
49
+ current[0] = leftIndex;
50
+
51
+ for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
52
+ const cost = left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1;
53
+
54
+ current[rightIndex] = Math.min(
55
+ current[rightIndex - 1] + 1,
56
+ previous[rightIndex] + 1,
57
+ previous[rightIndex - 1] + cost,
58
+ );
59
+ }
60
+
61
+ [previous, current] = [current, previous];
62
+ }
63
+
64
+ return previous[right.length] ?? 0;
65
+ };
66
+
67
+ /**
68
+ * Factories for the policy that decides when a saved document becomes a
69
+ * checkpoint. Pass the result to `createMdKitBackend`, not to your store.
70
+ *
71
+ * @remarks
72
+ * `smart()` applies the default autosave-friendly heuristic; `function()`
73
+ * receives mdkit's computed edit distance alongside the raw content so you can
74
+ * build a custom rule.
75
+ */
76
+ export const CheckpointPolicy = {
77
+ always: (): MdKitCheckpointPolicy => ({
78
+ shouldCheckpoint: () => true,
79
+ }),
80
+ function: (
81
+ shouldCheckpoint: MdKitCheckpointPolicy["shouldCheckpoint"],
82
+ ): MdKitCheckpointPolicy => ({
83
+ shouldCheckpoint,
84
+ }),
85
+ never: (): MdKitCheckpointPolicy => ({
86
+ shouldCheckpoint: () => false,
87
+ }),
88
+ smart: (
89
+ options: MdKitSmartCheckpointPolicyOptions = {},
90
+ ): MdKitCheckpointPolicy => {
91
+ const minEditDistance =
92
+ options.minEditDistance ?? defaultMinEditDistance;
93
+ const minIntervalMs = options.minIntervalMs ?? defaultMinIntervalMs;
94
+
95
+ return {
96
+ shouldCheckpoint: ({
97
+ editDistance,
98
+ previousCheckpoint,
99
+ timeSinceLastCheckpointMs,
100
+ }) =>
101
+ !previousCheckpoint ||
102
+ editDistance >= minEditDistance ||
103
+ (timeSinceLastCheckpointMs !== null &&
104
+ timeSinceLastCheckpointMs >= minIntervalMs),
105
+ };
106
+ },
107
+ } as const;