@hocuspocus/provider 2.9.1-rc.0 → 2.10.0

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.
@@ -121,7 +121,7 @@ export declare class HocuspocusProvider extends EventEmitter {
121
121
  set synced(state: boolean);
122
122
  receiveStateless(payload: string): void;
123
123
  get isAuthenticationRequired(): boolean;
124
- connect(): Promise<unknown>;
124
+ connect(): Promise<any>;
125
125
  disconnect(): void;
126
126
  onOpen(event: Event): Promise<void>;
127
127
  getToken(): Promise<string | null>;
@@ -102,7 +102,7 @@ export declare class HocuspocusProviderWebsocket extends EventEmitter {
102
102
  receivedOnStatusPayload?: onStatusParameters | undefined;
103
103
  onOpen(event: Event): Promise<void>;
104
104
  onStatus(data: onStatusParameters): Promise<void>;
105
- attach(provider: HocuspocusProvider): void;
105
+ attach(provider: HocuspocusProvider): Promise<any> | undefined;
106
106
  detach(provider: HocuspocusProvider): void;
107
107
  setConfiguration(configuration?: Partial<HocuspocusProviderWebsocketConfiguration>): void;
108
108
  cancelWebsocketRetry?: () => void;
@@ -1,7 +1,7 @@
1
1
  import type { AbstractType, YArrayEvent } from 'yjs';
2
2
  import { HocuspocusProvider, HocuspocusProviderConfiguration } from './HocuspocusProvider.js';
3
3
  import { TiptapCollabProviderWebsocket } from './TiptapCollabProviderWebsocket.js';
4
- import type { THistoryVersion } from './types.js';
4
+ import type { TCollabComment, TCollabThread, THistoryVersion } from './types.js';
5
5
  export type TiptapCollabProviderConfiguration = Required<Pick<HocuspocusProviderConfiguration, 'name'>> & Partial<HocuspocusProviderConfiguration> & (Required<Pick<AdditionalTiptapCollabProviderConfiguration, 'websocketProvider'>> | Required<Pick<AdditionalTiptapCollabProviderConfiguration, 'appId'>> | Required<Pick<AdditionalTiptapCollabProviderConfiguration, 'baseUrl'>>);
6
6
  export interface AdditionalTiptapCollabProviderConfiguration {
7
7
  /**
@@ -40,4 +40,19 @@ export declare class TiptapCollabProvider extends HocuspocusProvider {
40
40
  isAutoVersioning(): boolean;
41
41
  enableAutoVersioning(): 1;
42
42
  disableAutoVersioning(): 0;
43
+ private getYThreads;
44
+ getThreads<Data, CommentData>(): TCollabThread<Data, CommentData>[];
45
+ private getThreadIndex;
46
+ getThread<Data, CommentData>(id: string): TCollabThread<Data, CommentData> | null;
47
+ private getYThread;
48
+ createThread(data: Omit<TCollabThread, '_id' | 'createdAt' | 'updatedAt' | 'comments'>): TCollabThread | null;
49
+ updateThread(id: TCollabThread['id'], data: Partial<Pick<TCollabThread, 'data' | 'resolvedAt'>>): TCollabThread | null;
50
+ deleteThread(id: TCollabThread['id']): void;
51
+ getThreadComments(threadId: TCollabThread['id']): TCollabComment[] | null;
52
+ getThreadComment(threadId: TCollabThread['id'], commentId: TCollabComment['id']): TCollabComment | null;
53
+ addComment(threadId: TCollabThread['id'], data: Omit<TCollabComment, 'id' | 'updatedAt' | 'createdAt'>): TCollabThread | null;
54
+ updateComment(threadId: TCollabThread['id'], commentId: TCollabComment['id'], data: Partial<Pick<TCollabComment, 'data' | 'content'>>): TCollabThread | null;
55
+ deleteComment(threadId: TCollabThread['id'], commentId: TCollabComment['id']): TCollabThread | null;
56
+ watchThreads(callback: () => void): void;
57
+ unwatchThreads(callback: () => void): void;
43
58
  }
@@ -84,6 +84,21 @@ export type StatesArray = {
84
84
  clientId: number;
85
85
  [key: string | number]: any;
86
86
  }[];
87
+ export type TCollabThread<Data = any, CommentData = any> = {
88
+ id: string;
89
+ createdAt: number;
90
+ updatedAt: number;
91
+ resolvedAt?: string;
92
+ comments: TCollabComment<CommentData>[];
93
+ data: Data;
94
+ };
95
+ export type TCollabComment<Data = any> = {
96
+ id: string;
97
+ createdAt: number;
98
+ updatedAt: number;
99
+ data: Data;
100
+ content: any;
101
+ };
87
102
  export type THistoryVersion = {
88
103
  name?: string;
89
104
  version: number;
@@ -1,2 +1,2 @@
1
- declare const _default: import("vite").UserConfigExport;
1
+ declare const _default: import("vite").UserConfig;
2
2
  export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hocuspocus/provider",
3
- "version": "2.9.1-rc.0",
3
+ "version": "2.10.0",
4
4
  "description": "hocuspocus provider",
5
5
  "homepage": "https://hocuspocus.dev",
6
6
  "keywords": [
@@ -29,7 +29,7 @@
29
29
  "dist"
30
30
  ],
31
31
  "dependencies": {
32
- "@hocuspocus/common": "^2.9.1-rc.0",
32
+ "@hocuspocus/common": "^2.10.0",
33
33
  "@lifeomic/attempt": "^3.0.2",
34
34
  "lib0": "^0.2.87",
35
35
  "ws": "^8.14.2"
@@ -367,7 +367,9 @@ export class HocuspocusProvider extends EventEmitter {
367
367
  this.subscribeToBroadcastChannel()
368
368
  }
369
369
 
370
- return this.configuration.websocketProvider.connect()
370
+ this.configuration.websocketProvider.shouldConnect = true
371
+
372
+ return this.configuration.websocketProvider.attach(this)
371
373
  }
372
374
 
373
375
  disconnect() {
@@ -217,10 +217,11 @@ export class HocuspocusProviderWebsocket extends EventEmitter {
217
217
  }
218
218
 
219
219
  attach(provider: HocuspocusProvider) {
220
+ let connectPromise: Promise<any> | undefined
220
221
  this.configuration.providerMap.set(provider.configuration.name, provider)
221
222
 
222
223
  if (this.status === WebSocketStatus.Disconnected && this.shouldConnect) {
223
- this.connect()
224
+ connectPromise = this.connect()
224
225
  }
225
226
 
226
227
  if (this.receivedOnOpenPayload) {
@@ -230,6 +231,8 @@ export class HocuspocusProviderWebsocket extends EventEmitter {
230
231
  if (this.receivedOnStatusPayload) {
231
232
  provider.onStatus(this.receivedOnStatusPayload)
232
233
  }
234
+
235
+ return connectPromise
233
236
  }
234
237
 
235
238
  detach(provider: HocuspocusProvider) {
@@ -1,11 +1,15 @@
1
1
  import type { AbstractType, YArrayEvent } from 'yjs'
2
+ import * as Y from 'yjs'
3
+ import { uuidv4 } from 'lib0/random'
2
4
  import {
3
5
  HocuspocusProvider,
4
6
  HocuspocusProviderConfiguration,
5
7
  } from './HocuspocusProvider.js'
6
8
 
7
9
  import { TiptapCollabProviderWebsocket } from './TiptapCollabProviderWebsocket.js'
8
- import type { THistoryVersion } from './types.js'
10
+ import type {
11
+ TCollabComment, TCollabThread, THistoryVersion,
12
+ } from './types.js'
9
13
 
10
14
  export type TiptapCollabProviderConfiguration =
11
15
  Required<Pick<HocuspocusProviderConfiguration, 'name'>> &
@@ -93,4 +97,181 @@ export class TiptapCollabProvider extends HocuspocusProvider {
93
97
  return this.configuration.document.getMap<number>(`${this.tiptapCollabConfigurationPrefix}config`).set('autoVersioning', 0)
94
98
  }
95
99
 
100
+ private getYThreads() {
101
+ return this.configuration.document.getArray<Y.Map<any>>(`${this.tiptapCollabConfigurationPrefix}threads`)
102
+ }
103
+
104
+ getThreads<Data, CommentData>(): TCollabThread<Data, CommentData>[] {
105
+ return this.getYThreads().toJSON() as TCollabThread<Data, CommentData>[]
106
+ }
107
+
108
+ private getThreadIndex(id: string): number | null {
109
+ let index = null
110
+
111
+ let i = 0
112
+ // eslint-disable-next-line no-restricted-syntax
113
+ for (const thread of this.getThreads()) {
114
+ if (thread.id === id) {
115
+ index = i
116
+ break
117
+ }
118
+ i += 1
119
+ }
120
+
121
+ return index
122
+ }
123
+
124
+ getThread<Data, CommentData>(id: string): TCollabThread<Data, CommentData> | null {
125
+ const index = this.getThreadIndex(id)
126
+
127
+ if (index === null) {
128
+ return null
129
+ }
130
+
131
+ return this.getYThreads().get(index).toJSON() as TCollabThread<Data, CommentData>
132
+ }
133
+
134
+ private getYThread(id: string) {
135
+ const index = this.getThreadIndex(id)
136
+
137
+ if (index === null) {
138
+ return null
139
+ }
140
+
141
+ return this.getYThreads().get(index)
142
+ }
143
+
144
+ createThread(data: Omit<TCollabThread, '_id' | 'createdAt' | 'updatedAt' | 'comments'>) {
145
+ const thread = new Y.Map()
146
+ thread.set('id', uuidv4())
147
+ thread.set('createdAt', (new Date()).toISOString())
148
+ thread.set('comments', new Y.Array())
149
+
150
+ this.getYThreads().push([thread])
151
+ return this.updateThread(String(thread.get('id')), data)
152
+ }
153
+
154
+ updateThread(id: TCollabThread['id'], data: Partial<Pick<TCollabThread, 'data' | 'resolvedAt'>>) {
155
+ const thread = this.getYThread(id)
156
+
157
+ if (thread === null) {
158
+ return null
159
+ }
160
+
161
+ thread.set('updatedAt', (new Date()).toISOString())
162
+
163
+ if (data.data) {
164
+ thread.set('data', data.data)
165
+ }
166
+
167
+ if (data.resolvedAt || data.resolvedAt === null) {
168
+ thread.set('resolvedAt', data.resolvedAt)
169
+ }
170
+
171
+ return thread.toJSON() as TCollabThread
172
+ }
173
+
174
+ deleteThread(id: TCollabThread['id']) {
175
+ const index = this.getThreadIndex(id)
176
+
177
+ if (index === null) {
178
+ return
179
+ }
180
+
181
+ this.getYThreads().delete(index, 1)
182
+ }
183
+
184
+ getThreadComments(threadId: TCollabThread['id']): TCollabComment[] | null {
185
+ const index = this.getThreadIndex(threadId)
186
+
187
+ if (index === null) {
188
+ return null
189
+ }
190
+
191
+ return this.getThread(threadId)?.comments ?? []
192
+ }
193
+
194
+ getThreadComment(threadId: TCollabThread['id'], commentId: TCollabComment['id']): TCollabComment | null {
195
+ const index = this.getThreadIndex(threadId)
196
+
197
+ if (index === null) {
198
+ return null
199
+ }
200
+
201
+ return this.getThread(threadId)?.comments.find(comment => comment.id === commentId) ?? null
202
+ }
203
+
204
+ addComment(threadId: TCollabThread['id'], data: Omit<TCollabComment, 'id' | 'updatedAt' | 'createdAt'>) {
205
+ const thread = this.getYThread(threadId)
206
+
207
+ if (thread === null) return null
208
+
209
+ const commentMap = new Y.Map()
210
+ commentMap.set('id', uuidv4())
211
+ commentMap.set('createdAt', (new Date()).toISOString())
212
+ thread.get('comments').push([commentMap])
213
+
214
+ this.updateComment(threadId, String(commentMap.get('id')), data)
215
+
216
+ return thread.toJSON() as TCollabThread
217
+ }
218
+
219
+ updateComment(threadId: TCollabThread['id'], commentId: TCollabComment['id'], data: Partial<Pick<TCollabComment, 'data' | 'content'>>) {
220
+ const thread = this.getYThread(threadId)
221
+
222
+ if (thread === null) return null
223
+
224
+ let comment = null
225
+ // eslint-disable-next-line no-restricted-syntax
226
+ for (const c of thread.get('comments')) {
227
+ if (c.get('id') === commentId) {
228
+ comment = c
229
+ break
230
+ }
231
+ }
232
+
233
+ if (comment === null) return null
234
+
235
+ comment.set('updatedAt', (new Date()).toISOString())
236
+
237
+ if (data.data) {
238
+ comment.set('data', data.data)
239
+ }
240
+
241
+ if (data.content) {
242
+ comment.set('content', data.content)
243
+ }
244
+
245
+ return thread.toJSON() as TCollabThread
246
+ }
247
+
248
+ deleteComment(threadId: TCollabThread['id'], commentId: TCollabComment['id']) {
249
+ const thread = this.getYThread(threadId)
250
+
251
+ if (thread === null) return null
252
+
253
+ let commentIndex = 0
254
+ // eslint-disable-next-line no-restricted-syntax
255
+ for (const c of thread.get('comments')) {
256
+ if (c.get('id') === commentId) {
257
+ break
258
+ }
259
+ commentIndex += 1
260
+ }
261
+
262
+ if (commentIndex >= 0) {
263
+ thread.get('comments').delete(commentIndex)
264
+ }
265
+
266
+ return thread.toJSON() as TCollabThread
267
+ }
268
+
269
+ watchThreads(callback: () => void) {
270
+ this.getYThreads().observeDeep(callback)
271
+ }
272
+
273
+ unwatchThreads(callback: () => void) {
274
+ this.getYThreads().unobserveDeep(callback)
275
+ }
276
+
96
277
  }
package/src/types.ts CHANGED
@@ -106,6 +106,23 @@ export type StatesArray = { clientId: number, [key: string | number]: any }[]
106
106
 
107
107
  // hocuspocus-pro types
108
108
 
109
+ export type TCollabThread<Data = any, CommentData = any> = {
110
+ id: string;
111
+ createdAt: number;
112
+ updatedAt: number;
113
+ resolvedAt?: string; // (new Date()).toISOString()
114
+ comments: TCollabComment<CommentData>[];
115
+ data: Data
116
+ }
117
+
118
+ export type TCollabComment<Data = any> = {
119
+ id: string;
120
+ createdAt: number;
121
+ updatedAt: number;
122
+ data: Data
123
+ content: any
124
+ }
125
+
109
126
  export type THistoryVersion = {
110
127
  name?: string;
111
128
  version: number;