11import * as Y from 'yjs' ;
2- import { Connection , Hocuspocus , type Extension } from '@hocuspocus/server' ;
2+ import { type Configuration , type Connection , Hocuspocus , type Extension } from '@hocuspocus/server' ;
33import { Logger } from '@hocuspocus/extension-logger' ;
44import { S3 } from '@hocuspocus/extension-s3' ;
5+ import { type Application } from 'express-ws' ;
56
67import {
78 fetchReport ,
@@ -23,123 +24,8 @@ export const s3Extension = new S3({
2324
2425const loggerExtention = new Logger ( ) ;
2526
26- // Configure hocuspocus server
27- export const hocuspocusServer = new Hocuspocus ( {
28- extensions : [
29- s3Extension satisfies Extension ,
30- loggerExtention satisfies Extension ,
31- ] ,
32- onConnect : async ( data ) => {
33- const { documentName, context } = data ;
34- const docInfo = validateDocName ( documentName ) ;
35-
36- if ( docInfo instanceof Error ) {
37- // NOTE: Throwing exception so that connection is not established
38- throw docInfo ;
39- }
40-
41- return {
42- ...context ,
43- doc : docInfo ,
44- } ;
45- } ,
46- onAuthenticate : async ( data ) => {
47- // NOTE: onAuthenticate will only be called if user supplied a "token"
48- // TODO: check what happens if no token is sent?
49- const { token, context } = data ;
50-
51- const tokenData = await verifyJwt (
52- token ,
53- env . WEB_COGNITO_USER_POOL_ID ,
54- env . WEB_COGNITO_USER_POOL_CLIENT_ID ,
55- env . COGNITO_ISSUER ,
56- 'id' ,
57- ) ;
58-
59- if ( tokenData instanceof Error ) {
60- // NOTE: Throwing exception so that authenitcation fails
61- throw Error ( 'Token must be valid!' ) ;
62- }
63-
64- // TODO: Update permissions from user group and pass permission function
65- const id = tokenData [ 'cognito:username' ] ;
66- const groups = tokenData [ 'cognito:groups' ] ;
67- if ( ! groups || groups . length <= 0 ) {
68- // NOTE: Throwing exception so that authenitcation fails
69- throw Error ( 'User should be in a group to edit documents' ) ;
70- }
71-
72- return {
73- ...context ,
74- user : {
75- id : id as string ,
76- username : tokenData . preferred_username as string ,
77- email : tokenData . email as string ,
78- groups : groups as string [ ] ,
79- token,
80- } ,
81- } ;
82- } ,
83- onLoadDocument : async ( data ) => {
84- // NOTE: We can load the data from extension directly if it exists in s3
85- const updateFromS3 = await s3Extension . configuration . fetch ( data ) ;
86- if ( updateFromS3 !== null ) {
87- Y . applyUpdate ( data . document , updateFromS3 ) ;
88- console . debug ( `Loaded document "${ data . documentName } " from s3` ) ;
89- return data . document ;
90- }
91-
92- // NOTE: This means we are creating a direct connection.
93- // In this case, we should not load data from API
94- if ( ! data . context || ! data . context . user ) {
95- // NOTE: Throwing exception so that empty document is not created
96- throw Error ( `Could not load document "${ data . documentName } " from s3` ) ;
97- }
98-
99- // NOTE: If document not in S3, get from server
100- const { token } = data . context . user ;
101- const { id } = data . context . doc ;
102- const reportV1 = await fetchReport (
103- env . BACKEND_HOST ,
104- id ,
105- token ,
106- ) ;
107-
108- if ( reportV1 instanceof Error ) {
109- // NOTE: Throwing exception so that empty document is not created
110- throw reportV1 ;
111- }
112-
113- slateReportToDoc ( reportV1 , data . document ) ;
114- console . debug ( `Loaded document "${ data . documentName } " from API` ) ;
115- return data . document ;
116- } ,
117- onChange : async ( data ) => {
118- const {
119- document,
120- transactionOrigin,
121- } = data ;
122-
123- // NOTE: We only want to update these counts when changes are from the client
124- const connection : Connection | null | undefined = transactionOrigin ;
125- if ( connection === null || connection === undefined ) {
126- return ;
127- }
128-
129- document . transact ( ( ) => {
130- changeReportUpdateStates (
131- document ,
132- oldValue => ( {
133- no_of_updates : ( oldValue ?. no_of_updates ?? 0 ) + 1 ,
134- last_updated : new Date ( ) . getTime ( ) ,
135- } ) ,
136- ) ;
137- } ) ;
138- } ,
139- } ) ;
140-
14127// Get a document from hocuspocus or s3
142- export async function getDoc ( name : string ) {
28+ export async function getDoc ( name : string , hocuspocusServer : Hocuspocus ) {
14329 const doc = hocuspocusServer . documents . get ( name ) ;
14430 if ( doc ) {
14531 return doc ;
@@ -155,3 +41,138 @@ export async function getDoc(name: string) {
15541 Y . applyUpdate ( s3Doc , fetched ) ;
15642 return s3Doc ;
15743}
44+
45+ export function registerHocuspocus ( app : Application , otherConfig ?: Partial < Configuration > ) {
46+ // Configure hocuspocus server
47+ const hocuspocusServer = new Hocuspocus ( {
48+ extensions : [
49+ s3Extension satisfies Extension ,
50+ loggerExtention satisfies Extension ,
51+ ] ,
52+ onConnect : async ( data ) => {
53+ const { documentName, context } = data ;
54+ const docInfo = validateDocName ( documentName ) ;
55+
56+ if ( docInfo instanceof Error ) {
57+ // NOTE: Throwing exception so that connection is not established
58+ throw docInfo ;
59+ }
60+
61+ return {
62+ ...context ,
63+ doc : docInfo ,
64+ } ;
65+ } ,
66+ onAuthenticate : async ( data ) => {
67+ // NOTE: onAuthenticate will only be called if user supplied a "token"
68+ // TODO: check what happens if no token is sent?
69+ const { token, context } = data ;
70+
71+ const tokenData = await verifyJwt (
72+ token ,
73+ env . WEB_COGNITO_USER_POOL_ID ,
74+ env . WEB_COGNITO_USER_POOL_CLIENT_ID ,
75+ env . COGNITO_ISSUER ,
76+ 'id' ,
77+ ) ;
78+
79+ if ( tokenData instanceof Error ) {
80+ // NOTE: Throwing exception so that authenitcation fails
81+ throw Error ( 'Token must be valid!' ) ;
82+ }
83+
84+ // TODO: Update permissions from user group and pass permission function
85+ const id = tokenData [ 'cognito:username' ] ;
86+ const groups = tokenData [ 'cognito:groups' ] ;
87+ if ( ! groups || groups . length <= 0 ) {
88+ // NOTE: Throwing exception so that authenitcation fails
89+ throw Error ( 'User should be in a group to edit documents' ) ;
90+ }
91+
92+ return {
93+ ...context ,
94+ user : {
95+ id : id as string ,
96+ username : tokenData . preferred_username as string ,
97+ email : tokenData . email as string ,
98+ groups : groups as string [ ] ,
99+ token,
100+ } ,
101+ } ;
102+ } ,
103+ onLoadDocument : async ( data ) => {
104+ // NOTE: We can load the data from extension directly if it exists in s3
105+ const updateFromS3 = await s3Extension . configuration . fetch ( data ) ;
106+ if ( updateFromS3 !== null ) {
107+ Y . applyUpdate ( data . document , updateFromS3 ) ;
108+ console . debug ( `Loaded document "${ data . documentName } " from s3` ) ;
109+ return data . document ;
110+ }
111+
112+ // NOTE: This means we are creating a direct connection.
113+ // In this case, we should not load data from API
114+ if ( ! data . context || ! data . context . user ) {
115+ // NOTE: Throwing exception so that empty document is not created
116+ throw Error ( `Could not load document "${ data . documentName } " from s3` ) ;
117+ }
118+
119+ // NOTE: If document not in S3, get from server
120+ const { token } = data . context . user ;
121+ const { id } = data . context . doc ;
122+ const reportV1 = await fetchReport (
123+ env . BACKEND_HOST ,
124+ id ,
125+ token ,
126+ ) ;
127+
128+ if ( reportV1 instanceof Error ) {
129+ // FIXME: Convert exception to CloseEvent
130+ // NOTE: Throwing exception so that empty document is not created
131+ throw {
132+ code : 9000 ,
133+ reason : reportV1 . message ,
134+ } ;
135+ }
136+
137+ slateReportToDoc ( reportV1 , data . document ) ;
138+ console . debug ( `Loaded document "${ data . documentName } " from API` ) ;
139+ return data . document ;
140+ } ,
141+ onChange : async ( data ) => {
142+ const {
143+ document,
144+ transactionOrigin,
145+ } = data ;
146+
147+ // NOTE: We only want to update these counts when changes are from the client
148+ const connection : Connection | null | undefined = transactionOrigin ;
149+ if ( connection === null || connection === undefined ) {
150+ return ;
151+ }
152+
153+ document . transact ( ( ) => {
154+ changeReportUpdateStates (
155+ document ,
156+ oldValue => ( {
157+ no_of_updates : ( oldValue ?. no_of_updates ?? 0 ) + 1 ,
158+ last_updated : new Date ( ) . getTime ( ) ,
159+ } ) ,
160+ ) ;
161+ } ) ;
162+ } ,
163+ ...otherConfig ,
164+ } ) ;
165+
166+ // NOTE: We are attaching hocuspocus so that we can access this later
167+ app . locals . hocuspocus = hocuspocusServer ;
168+
169+ app . ws ( '/connect/' , ( websocket , request ) => {
170+ hocuspocusServer . handleConnection ( websocket , request ) ;
171+ } ) ;
172+
173+ app . ws ( '/collab/' , ( websocket , request ) => {
174+ hocuspocusServer . handleConnection ( websocket , request ) ;
175+ } ) ;
176+
177+ return hocuspocusServer ;
178+ }
0 commit comments