Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/components/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -571,14 +571,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 @@ -92,6 +92,8 @@ class SyncService {

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

this.pushError = 0

return this
}

Expand All @@ -103,10 +105,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 connect = initialSession
? Promise.resolve(new Connection({ data: initialSession }, {}))
Expand All @@ -115,19 +130,15 @@ class SyncService {
this.#connection = await connect
if (!this.#connection) {
// 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 @@ -181,6 +192,7 @@ class SyncService {
}
return this.#connection.push(data)
.then((response) => {
this.pushError = 0
this.sending = false
this.emit('sync', {
steps: [],
Expand All @@ -190,6 +202,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 @@ -206,6 +219,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 @@ -316,7 +331,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