-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨(frontend) add long polling falback system
When the websocket cannot connect, after a certain amount of retry, the system will fallback to long polling to keep the document sync. If the websocket is connected, the system will automatically switch back to websocket and stop long polling.
- Loading branch information
Showing
9 changed files
with
234 additions
and
74 deletions.
There are no files selected for viewing
106 changes: 106 additions & 0 deletions
106
src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import { expect, test } from '@playwright/test'; | ||
|
||
import { createDoc } from './common'; | ||
|
||
test.beforeEach(async ({ page }) => { | ||
await page.goto('/'); | ||
}); | ||
|
||
test.describe('Doc Collaboration', () => { | ||
/** | ||
* We check: | ||
* - connection to the collaborative server | ||
* - signal of the backend to the collaborative server (connection should close) | ||
* - reconnection to the collaborative server | ||
*/ | ||
test('checks the connection with collaborative server', async ({ | ||
page, | ||
browserName, | ||
}) => { | ||
let webSocketPromise = page.waitForEvent('websocket', (webSocket) => { | ||
return webSocket | ||
.url() | ||
.includes('ws://localhost:8083/collaboration/ws/?room='); | ||
}); | ||
|
||
const randomDoc = await createDoc(page, 'doc-editor', browserName, 1); | ||
await expect(page.locator('h2').getByText(randomDoc[0])).toBeVisible(); | ||
|
||
let webSocket = await webSocketPromise; | ||
expect(webSocket.url()).toContain( | ||
'ws://localhost:8083/collaboration/ws/?room=', | ||
); | ||
|
||
// Is connected | ||
let framesentPromise = webSocket.waitForEvent('framesent'); | ||
|
||
await page.locator('.ProseMirror.bn-editor').click(); | ||
await page.locator('.ProseMirror.bn-editor').fill('Hello World'); | ||
|
||
let framesent = await framesentPromise; | ||
expect(framesent.payload).not.toBeNull(); | ||
|
||
await page.getByRole('button', { name: 'Share' }).click(); | ||
|
||
const selectVisibility = page.getByRole('combobox', { | ||
name: 'Visibility', | ||
}); | ||
|
||
// When the visibility is changed, the ws should closed the connection (backend signal) | ||
const wsClosePromise = webSocket.waitForEvent('close'); | ||
|
||
await selectVisibility.click(); | ||
await page | ||
.getByRole('option', { | ||
name: 'Authenticated', | ||
}) | ||
.click(); | ||
|
||
// Assert that the doc reconnects to the ws | ||
const wsClose = await wsClosePromise; | ||
expect(wsClose.isClosed()).toBeTruthy(); | ||
|
||
// Checkt the ws is connected again | ||
webSocketPromise = page.waitForEvent('websocket', (webSocket) => { | ||
return webSocket | ||
.url() | ||
.includes('ws://localhost:8083/collaboration/ws/?room='); | ||
}); | ||
|
||
webSocket = await webSocketPromise; | ||
framesentPromise = webSocket.waitForEvent('framesent'); | ||
framesent = await framesentPromise; | ||
expect(framesent.payload).not.toBeNull(); | ||
}); | ||
|
||
test('checks the connection switch to polling after websocket failure', async ({ | ||
page, | ||
browserName, | ||
}) => { | ||
const responsePromise = page.waitForResponse( | ||
(response) => | ||
response.url().includes('/poll/') && response.status() === 200, | ||
); | ||
|
||
await page.routeWebSocket( | ||
'ws://localhost:8083/collaboration/ws/**', | ||
async (ws) => { | ||
await ws.close(); | ||
}, | ||
); | ||
|
||
await page.reload(); | ||
|
||
const randomDoc = await createDoc(page, 'doc-polling', browserName, 1); | ||
await expect(page.locator('h2').getByText(randomDoc[0])).toBeVisible(); | ||
|
||
const response = await responsePromise; | ||
const responseJson = (await response.json()) as { | ||
connectionsCount: number; | ||
yDoc64?: string; | ||
}; | ||
|
||
expect(responseJson.yDoc64).toBeDefined(); | ||
expect(responseJson.connectionsCount).toBe(0); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletions
1
src/frontend/apps/impress/src/features/docs/doc-management/api/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
34 changes: 34 additions & 0 deletions
34
src/frontend/apps/impress/src/features/docs/doc-management/api/syncDocPolling.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { APIError, errorCauses } from '@/api'; | ||
|
||
import { Base64 } from '../types'; | ||
|
||
interface SyncDocPollingParams { | ||
pollUrl: string; | ||
yDoc64: Base64; | ||
} | ||
|
||
interface SyncDocPollingResponse { | ||
yDoc64?: Base64; | ||
} | ||
|
||
export const syncDocPolling = async ({ | ||
pollUrl, | ||
yDoc64, | ||
}: SyncDocPollingParams): Promise<SyncDocPollingResponse> => { | ||
const response = await fetch(pollUrl, { | ||
method: 'POST', | ||
credentials: 'include', | ||
headers: { | ||
'Content-Type': 'application/json', | ||
}, | ||
body: JSON.stringify({ | ||
yDoc64, | ||
}), | ||
}); | ||
|
||
if (!response.ok) { | ||
throw new APIError('Failed to sync the doc', await errorCauses(response)); | ||
} | ||
|
||
return response.json() as Promise<SyncDocPollingResponse>; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters