Skip to content

Commit

Permalink
Merge pull request #6204 from nextcloud/backport/6200/stable28
Browse files Browse the repository at this point in the history
[stable28] Fix reconnecting websocket polyfill and error propagation during push
  • Loading branch information
juliushaertl authored Aug 28, 2024
2 parents 461f605 + 78f1461 commit 5e00fb7
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 26 deletions.
4 changes: 2 additions & 2 deletions src/components/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -562,14 +562,14 @@ export default {
this.document = document
this.syncError = null
const editable = !this.readOnly
const editable = !this.readOnly && !this.hasConnectionIssue
if (this.$editor.isEditable !== editable) {
this.$editor.setEditable(editable)
}
},
onSync({ steps, document }) {
this.hasConnectionIssue = false
this.hasConnectionIssue = this.$syncService.backend.fetcher === 0 || !this.$providers[0].wsconnected || this.$syncService.pushError > 0
this.$nextTick(() => {
this.emit('sync-service:sync')
})
Expand Down
41 changes: 28 additions & 13 deletions src/services/SyncService.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ class SyncService {

this.autosave = debounce(this._autosave.bind(this), AUTOSAVE_INTERVAL)

this.pushError = 0

return this
}

Expand All @@ -106,10 +108,23 @@ class SyncService {
return this.#connection.session.guestName
}

get hasActiveConnection() {
return this.#connection && !this.#connection.isClosed
}

get connectionState() {
if (!this.#connection || this.version === undefined) {
return null
}
return {
...this.#connection.state,
version: this.version,
}
}

async open({ fileId, initialSession }) {
if (this.#connection && !this.#connection.isClosed) {
// We're already connected.
return
if (this.hasActiveConnection) {
return this.connectionState
}
const onChange = ({ sessions }) => {
this.sessions = sessions
Expand All @@ -124,19 +139,15 @@ class SyncService {
if (!this.#connection) {
this.off('change', onChange)
// Error was already emitted in connect
return
return null
}
this.backend = new PollingBackend(this, this.#connection)
this.version = this.#connection.docStateVersion
this.baseVersionEtag = this.#connection.document.baseVersionEtag
this.emit('opened', {
...this.#connection.state,
version: this.version,
})
this.emit('loaded', {
...this.#connection.state,
version: this.version,
})
this.emit('opened', this.connectionState)
this.emit('loaded', this.connectionState)

return this.connectionState
}

startSync() {
Expand Down Expand Up @@ -190,6 +201,7 @@ class SyncService {
}
return this.#connection.push(data)
.then((response) => {
this.pushError = 0
this.sending = false
this.emit('sync', {
steps: [],
Expand All @@ -199,6 +211,7 @@ class SyncService {
}).catch(err => {
const { response, code } = err
this.sending = false
this.pushError++
if (!response || code === 'ECONNABORTED') {
this.emit('error', { type: ERROR_TYPE.CONNECTION_FAILED, data: {} })
}
Expand All @@ -215,6 +228,8 @@ class SyncService {
this.emit('error', { type: ERROR_TYPE.PUSH_FAILURE, data: {} })
OC.Notification.showTemporary('Changes could not be sent yet')
}
} else {
this.emit('error', { type: ERROR_TYPE.PUSH_FAILURE, data: {} })
}
throw new Error('Failed to apply steps. Retry!', { cause: err })
})
Expand Down Expand Up @@ -325,7 +340,7 @@ class SyncService {
// Make sure to leave no pending requests behind.
this.autosave.clear()
this.backend?.disconnect()
if (!this.#connection || this.#connection.isClosed) {
if (!this.hasActiveConnection) {
return
}
return this.#connection.close()
Expand Down
19 changes: 15 additions & 4 deletions src/services/WebSocketPolyfill.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,9 @@ export default function initWebSocketPolyfill(syncService, fileId, initialSessio
logger.debug('WebSocketPolyfill#constructor', { url, fileId, initialSession })
this.#registerHandlers({
opened: ({ version, session }) => {
this.#version = version
logger.debug('opened ', { version, session })
this.#version = version
this.#session = session
this.onopen?.()
},
loaded: ({ version, session, content }) => {
logger.debug('loaded ', { version, session })
Expand All @@ -70,7 +69,16 @@ export default function initWebSocketPolyfill(syncService, fileId, initialSessio
}
},
})
syncService.open({ fileId, initialSession })

syncService.open({ fileId, initialSession }).then((data) => {
if (syncService.hasActiveConnection) {
const { version, session } = data
this.#version = version
this.#session = session

this.onopen?.()
}
})
}

#registerHandlers(handlers) {
Expand Down Expand Up @@ -101,7 +109,10 @@ export default function initWebSocketPolyfill(syncService, fileId, initialSessio
...queue.filter(s => !outbox.includes(s)),
)
return ret
}, err => logger.error(err))
}, err => {
logger.error(`Failed to push the queue with ${queue.length} steps to the server`, err)
this.onerror?.(err)
})
}

async close() {
Expand Down
17 changes: 10 additions & 7 deletions src/tests/services/WebsocketPolyfill.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@ import initWebSocketPolyfill from '../../services/WebSocketPolyfill.js'
describe('Init function', () => {

it('returns a websocket polyfill class', () => {
const syncService = { on: jest.fn(), open: jest.fn() }
const syncService = { on: jest.fn(), open: jest.fn(() => Promise.resolve({ version: 123, session: {} })) }
const Polyfill = initWebSocketPolyfill(syncService)
const websocket = new Polyfill('url')
expect(websocket).toBeInstanceOf(Polyfill)
})

it('registers handlers', () => {
const syncService = { on: jest.fn(), open: jest.fn() }
const syncService = { on: jest.fn(), open: jest.fn(() => Promise.resolve({ version: 123, session: {} })) }
const Polyfill = initWebSocketPolyfill(syncService)
const websocket = new Polyfill('url')
expect(syncService.on).toHaveBeenCalled()
})

it('opens sync service', () => {
const syncService = { on: jest.fn(), open: jest.fn() }
const syncService = { on: jest.fn(), open: jest.fn(() => Promise.resolve({ version: 123, session: {} })) }
const fileId = 123
const initialSession = { }
const Polyfill = initWebSocketPolyfill(syncService, fileId, initialSession)
Expand All @@ -28,7 +28,7 @@ describe('Init function', () => {
it('sends steps to sync service', async () => {
const syncService = {
on: jest.fn(),
open: jest.fn(),
open: jest.fn(() => Promise.resolve({ version: 123, session: {} })),
sendSteps: async getData => getData(),
}
const queue = [ 'initial' ]
Expand All @@ -46,9 +46,10 @@ describe('Init function', () => {
})

it('handles early reject', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {})
const syncService = {
on: jest.fn(),
open: jest.fn(),
open: jest.fn(() => Promise.resolve({ version: 123, session: {} })),
sendSteps: jest.fn().mockRejectedValue('error before reading steps in sync service'),
}
const queue = [ 'initial' ]
Expand All @@ -64,9 +65,10 @@ describe('Init function', () => {
})

it('handles reject after reading data', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {})
const syncService = {
on: jest.fn(),
open: jest.fn(),
open: jest.fn(() => Promise.resolve({ version: 123, session: {} })),
sendSteps: jest.fn().mockImplementation( async getData => {
getData()
throw 'error when sending in sync service'
Expand All @@ -85,9 +87,10 @@ describe('Init function', () => {
})

it('queue survives a close', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {})
const syncService = {
on: jest.fn(),
open: jest.fn(),
open: jest.fn(() => Promise.resolve({ version: 123, session: {} })),
sendSteps: jest.fn().mockImplementation( async getData => {
getData()
throw 'error when sending in sync service'
Expand Down

0 comments on commit 5e00fb7

Please sign in to comment.