Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
95764c6
chore(split): SaveService from SyncService
max-nextcloud Jul 1, 2025
2405e75
chore(type): sync service with typescript
max-nextcloud Jul 2, 2025
8525912
chore(migrate): sync service mixin to composable
max-nextcloud Jul 2, 2025
507a7a5
chore(refactor): watch sync service to create save service
max-nextcloud Jul 3, 2025
594fb1a
chore(refactor): move connectSyncService into useSyncService composable
max-nextcloud Jul 3, 2025
725872c
refactor(compose): migrate save service to composable
max-nextcloud Jul 3, 2025
9fdf878
refactor(editor): detect rich editor based on markdown extension
max-nextcloud Jul 3, 2025
94f7619
refactor(editor): always provide an editor
max-nextcloud Jul 3, 2025
e8d184a
refactor(cleanup): unwrap connection
max-nextcloud Jul 3, 2025
c856f2f
chore(minor): clean up redundant injects
max-nextcloud Jul 3, 2025
00c18c4
chore(refactor): simplify types for props
max-nextcloud Jul 3, 2025
c71503e
chore(refactor): watch sync service in useConnection
max-nextcloud Jul 5, 2025
7265f02
chore(extract): Mentions api from extension
max-nextcloud Jul 5, 2025
f6b76f3
chore(simplify): SyncService.open returns void
max-nextcloud Jul 5, 2025
90ae016
chore(simplify): combine loaded and opened event
max-nextcloud Jul 5, 2025
8a46d28
chore(cleanup): unused getter
max-nextcloud Jul 5, 2025
798c4b8
fix(menu): call base components setup function
max-nextcloud Jul 5, 2025
8b45a52
chore(refactor): connect from useConnection composable
max-nextcloud Jul 5, 2025
7bc34c9
chore(cleanup): sync and save service are always defined now
max-nextcloud Jul 6, 2025
d4a09c4
test(cy): properly close connections
max-nextcloud Jul 6, 2025
e6fc63b
chore(refactor): sync service with new connection
max-nextcloud Jul 7, 2025
c3230d3
chore(refactor): instantiate SessionConnection with plain data
max-nextcloud Jul 7, 2025
7dc8e25
fix(sync): stop autosave when closing connection
max-nextcloud Jul 8, 2025
7c52d1c
chore(cleanup): ? on attributes that are always truthy
max-nextcloud Jul 8, 2025
f370913
chore(cleanup): avoid reuse of isRichEditor name
max-nextcloud Jul 8, 2025
6c76f27
chore(cleanup): remove outdated comment
max-nextcloud Jul 8, 2025
9ea9857
chore(copyright): fix year to 2025
max-nextcloud Jul 8, 2025
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
Prev Previous commit
Next Next commit
chore(refactor): instantiate SessionConnection with plain data
Signed-off-by: Max <[email protected]>
  • Loading branch information
max-nextcloud committed Jul 7, 2025
commit c3230d3613af9ac66a0e380c8f58b41d401f7977
2 changes: 1 addition & 1 deletion cypress/support/sessions.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const url = Cypress.config('baseUrl').replace(/\/index.php\/?$/g, '')

Cypress.Commands.add('createTextSession', async (fileId, options = {}) => {
const { connection, data } = await open({ fileId, token: options.shareToken, ...options })
return new SessionConnection({ data }, connection)
return new SessionConnection(data, connection)
})

Cypress.Commands.add('destroySession', async (sessionConnection) => {
Expand Down
4 changes: 2 additions & 2 deletions src/services/SessionConnection.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
#hasOwner
connection

constructor(response, connection) {
constructor(data, connection) {
const {
document,
session,
Expand All @@ -35,7 +35,7 @@
content,
documentState,
hasOwner,
} = response.data
} = data
this.#document = document
this.#session = session
this.#lock = lock
Expand Down Expand Up @@ -82,14 +82,14 @@
documentId: this.#document.id,
sessionId: this.#session.id,
sessionToken: this.#session.token,
token: this.connection.shareToken,

Check warning on line 85 in src/services/SessionConnection.js

View check run for this annotation

Codecov / codecov/patch

src/services/SessionConnection.js#L85

Added line #L85 was not covered by tests
}
}

sync({ version }) {
return this.#post(this.#url(`session/${this.#document.id}/sync`), {
...this.#defaultParams,
filePath: this.connection.filePath,

Check warning on line 92 in src/services/SessionConnection.js

View check run for this annotation

Codecov / codecov/patch

src/services/SessionConnection.js#L92

Added line #L92 was not covered by tests
baseVersionEtag: this.#document.baseVersionEtag,
version,
})
Expand All @@ -99,7 +99,7 @@
const url = this.#url(`session/${this.#document.id}/save`)
const postData = {
...this.#defaultParams,
filePath: this.connection.filePath,

Check warning on line 102 in src/services/SessionConnection.js

View check run for this annotation

Codecov / codecov/patch

src/services/SessionConnection.js#L102

Added line #L102 was not covered by tests
baseVersionEtag: this.#document.baseVersionEtag,
...data,
}
Expand All @@ -111,7 +111,7 @@
const url = this.#url(`session/${this.#document.id}/save`)
const postData = {
...this.#defaultParams,
filePath: this.connection.filePath,

Check warning on line 114 in src/services/SessionConnection.js

View check run for this annotation

Codecov / codecov/patch

src/services/SessionConnection.js#L114

Added line #L114 was not covered by tests
baseVersionEtag: this.#document.baseVersionEtag,
...data,
requestToken: getRequestToken() ?? '',
Expand Down Expand Up @@ -145,7 +145,7 @@
+ '&sessionToken='
+ encodeURIComponent(this.#session.token)
+ '&token='
+ encodeURIComponent(this.connection.shareToken || '')

Check warning on line 148 in src/services/SessionConnection.js

View check run for this annotation

Codecov / codecov/patch

src/services/SessionConnection.js#L148

Added line #L148 was not covered by tests
return this.#post(url, formData, {
headers: {
'Content-Type': 'multipart/form-data',
Expand All @@ -172,7 +172,7 @@
}

close() {
this.closed = true

Check warning on line 175 in src/services/SessionConnection.js

View check run for this annotation

Codecov / codecov/patch

src/services/SessionConnection.js#L175

Added line #L175 was not covered by tests
}

// To be used in Cypress tests only
Expand Down
5 changes: 1 addition & 4 deletions src/services/SyncService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,16 +134,16 @@
}

get isReadOnly() {
return this.sessionConnection?.state.document.readOnly
}

Check warning on line 138 in src/services/SyncService.ts

View check run for this annotation

Codecov / codecov/patch

src/services/SyncService.ts#L137-L138

Added lines #L137 - L138 were not covered by tests

get hasOwner() {
return this.sessionConnection?.hasOwner
}

get guestName() {
return this.sessionConnection?.session.guestName
}

Check warning on line 146 in src/services/SyncService.ts

View check run for this annotation

Codecov / codecov/patch

src/services/SyncService.ts#L145-L146

Added lines #L145 - L146 were not covered by tests

hasActiveConnection(): this is {
sessionConnection: SessionConnection
Expand All @@ -154,135 +154,132 @@

async open() {
if (this.hasActiveConnection()) {
return
}

Check warning on line 158 in src/services/SyncService.ts

View check run for this annotation

Codecov / codecov/patch

src/services/SyncService.ts#L157-L158

Added lines #L157 - L158 were not covered by tests
const data = await this.#openConnection().catch((e) => this._emitError(e))
if (!data) {
// Error was already emitted above
return
}

Check warning on line 163 in src/services/SyncService.ts

View check run for this annotation

Codecov / codecov/patch

src/services/SyncService.ts#L162-L163

Added lines #L162 - L163 were not covered by tests
this.sessionConnection = new SessionConnection(
{ data },
this.connection.value,
)
this.sessionConnection = new SessionConnection(data, this.connection.value)
this.backend = new PollingBackend(this, this.sessionConnection)
this.version = this.sessionConnection.docStateVersion
this.emit('opened', this.sessionConnection.state)
}

startSync() {
this.backend?.connect()
}

Check warning on line 172 in src/services/SyncService.ts

View check run for this annotation

Codecov / codecov/patch

src/services/SyncService.ts#L171-L172

Added lines #L171 - L172 were not covered by tests

syncUp() {
this.backend?.resetRefetchTimer()
}

Check warning on line 176 in src/services/SyncService.ts

View check run for this annotation

Codecov / codecov/patch

src/services/SyncService.ts#L175-L176

Added lines #L175 - L176 were not covered by tests

_emitError(error: { response?: object; code?: string }) {
if (!error.response || error.code === 'ECONNABORTED') {
this.emit('error', { type: ERROR_TYPE.CONNECTION_FAILED, data: {} })
} else {
this.emit('error', { type: ERROR_TYPE.LOAD_ERROR, data: error.response })
}
}

Check warning on line 184 in src/services/SyncService.ts

View check run for this annotation

Codecov / codecov/patch

src/services/SyncService.ts#L179-L184

Added lines #L179 - L184 were not covered by tests

updateSession(guestName: string) {
if (!this.sessionConnection?.isPublic) {
return Promise.reject(new Error())
}
return this.sessionConnection.update(guestName).catch((error) => {
logger.error('Failed to update the session', { error })
return Promise.reject(error)
})
}

Check warning on line 194 in src/services/SyncService.ts

View check run for this annotation

Codecov / codecov/patch

src/services/SyncService.ts#L187-L194

Added lines #L187 - L194 were not covered by tests

sendStep(step: ArrayBuffer) {
this.#outbox.storeStep(step)
this.sendSteps()
}

Check warning on line 199 in src/services/SyncService.ts

View check run for this annotation

Codecov / codecov/patch

src/services/SyncService.ts#L197-L199

Added lines #L197 - L199 were not covered by tests

sendSteps() {
// If already waiting to send, do nothing.
if (this.#sendIntervalId) {
return
}
this.#sendIntervalId = setInterval(() => {
if (this.sessionConnection && !this.#sending) {
this.sendStepsNow().catch((err) => logger.error(err))
}
}, 200)
}

Check warning on line 211 in src/services/SyncService.ts

View check run for this annotation

Codecov / codecov/patch

src/services/SyncService.ts#L203-L211

Added lines #L203 - L211 were not covered by tests

async sendStepsNow() {
this.#sending = true
clearInterval(this.#sendIntervalId)
this.#sendIntervalId = undefined
const sendable = this.#outbox.getDataToSend()
if (sendable.steps.length > 0) {
this.emit('stateChange', { dirty: true })
}
if (!this.hasActiveConnection()) {
return
}
return push(this.connection, {
version: this.version,
...sendable,
})
.then((response) => {
this.#outbox.clearSentData(sendable)
const { steps, documentState } = response.data as {

Check warning on line 230 in src/services/SyncService.ts

View check run for this annotation

Codecov / codecov/patch

src/services/SyncService.ts#L214-L230

Added lines #L214 - L230 were not covered by tests
steps: Step[]
documentState: string
}
if (documentState) {
const documentStateStep = documentStateToStep(documentState)
this.emit('sync', {
version: this.version,
steps: [documentStateStep],
document: this.sessionConnection.document,
})
}
this.pushError = 0
this.#sending = false
if (steps?.length > 0) {
this.receiveSteps({ steps })
}
})
.catch((err) => {
const { response, code } = err
this.#sending = false
this.pushError++
logger.error('Failed to push the steps to the server', err)
if (!response || code === 'ECONNABORTED') {
this.emit('error', {
type: ERROR_TYPE.CONNECTION_FAILED,
data: {},
})
}
if (response?.status === 412) {
this.emit('error', {
type: ERROR_TYPE.LOAD_ERROR,
data: response,
})
} else if (response?.status === 403) {

Check warning on line 264 in src/services/SyncService.ts

View check run for this annotation

Codecov / codecov/patch

src/services/SyncService.ts#L234-L264

Added lines #L234 - L264 were not covered by tests
// either the session is invalid or the document is read only.
logger.error('failed to write to document - not allowed')
this.emit('error', {
type: ERROR_TYPE.PUSH_FORBIDDEN,
data: {},
})
} else {
this.emit('error', { type: ERROR_TYPE.PUSH_FAILURE, data: {} })
}
throw new Error('Failed to apply steps. Retry!', { cause: err })
})
}

Check warning on line 276 in src/services/SyncService.ts

View check run for this annotation

Codecov / codecov/patch

src/services/SyncService.ts#L266-L276

Added lines #L266 - L276 were not covered by tests

receiveSteps({
steps,
document,
sessions = [],
}: {

Check warning on line 282 in src/services/SyncService.ts

View check run for this annotation

Codecov / codecov/patch

src/services/SyncService.ts#L279-L282

Added lines #L279 - L282 were not covered by tests
steps: { data: string[]; version: number; sessionId: number }[]
document?: object
sessions?: {
Expand All @@ -290,97 +287,97 @@
lastAwarenessMessage: string
clientId: number
}[]
}) {
const awareness = sessions
.filter(
(s) =>
s.lastContact
> Math.floor(Date.now() / 1000) - COLLABORATOR_DISCONNECT_TIME,
)
.filter((s) => s.lastAwarenessMessage)
.map((s) => {
return { step: s.lastAwarenessMessage, clientId: s.clientId }
})
const newSteps = [...awareness]
for (let i = 0; i < steps.length; i++) {
const singleSteps = steps[i].data
if (this.version < steps[i].version) {
this.version = steps[i].version
}
if (!Array.isArray(singleSteps)) {
logger.error('Invalid step data, skipping step', { step: steps[i] })

Check warning on line 308 in src/services/SyncService.ts

View check run for this annotation

Codecov / codecov/patch

src/services/SyncService.ts#L290-L308

Added lines #L290 - L308 were not covered by tests
// TODO: recover
continue
}
singleSteps.forEach((step) => {
newSteps.push({
step,
clientId: steps[i].sessionId,
})
})
}
this.#lastStepPush = Date.now()
this.emit('sync', {
steps: newSteps,
document,
version: this.version,
})
}

Check warning on line 325 in src/services/SyncService.ts

View check run for this annotation

Codecov / codecov/patch

src/services/SyncService.ts#L310-L325

Added lines #L310 - L325 were not covered by tests

checkIdle() {
const lastPushMinutesAgo = (Date.now() - this.#lastStepPush) / 1000 / 60
if (lastPushMinutesAgo > IDLE_TIMEOUT) {
logger.debug(
`[SyncService] Document is idle for ${IDLE_TIMEOUT} minutes, suspending connection`,
)
this.emit('idle')
return true
}
return false
}

Check warning on line 337 in src/services/SyncService.ts

View check run for this annotation

Codecov / codecov/patch

src/services/SyncService.ts#L328-L337

Added lines #L328 - L337 were not covered by tests

async sendRemainingSteps() {
if (!this.#outbox.hasUpdate) {
return
}
logger.debug('sending final steps')
return this.sendStepsNow().catch((err) => logger.error(err))
}

Check warning on line 345 in src/services/SyncService.ts

View check run for this annotation

Codecov / codecov/patch

src/services/SyncService.ts#L340-L345

Added lines #L340 - L345 were not covered by tests

async close() {
// Make sure to leave no pending requests behind.
this.backend?.disconnect()
if (this.connection.value) {
close(this.connection.value)

Check warning on line 351 in src/services/SyncService.ts

View check run for this annotation

Codecov / codecov/patch

src/services/SyncService.ts#L349-L351

Added lines #L349 - L351 were not covered by tests
// Log and ignore possible network issues.
.catch((e) => {
logger.info('Failed to close connection.', { e })
})
}

Check warning on line 356 in src/services/SyncService.ts

View check run for this annotation

Codecov / codecov/patch

src/services/SyncService.ts#L353-L356

Added lines #L353 - L356 were not covered by tests
// Mark sessionConnection closed so hasActiveConnection turns false and we can reconnect.
this.sessionConnection?.close()
}

Check warning on line 359 in src/services/SyncService.ts

View check run for this annotation

Codecov / codecov/patch

src/services/SyncService.ts#L358-L359

Added lines #L358 - L359 were not covered by tests

uploadAttachment(file: object) {
if (!this.hasActiveConnection()) {
throw new Error('Not connected to server.')
}
return this.sessionConnection.uploadAttachment(file)
}

Check warning on line 366 in src/services/SyncService.ts

View check run for this annotation

Codecov / codecov/patch

src/services/SyncService.ts#L362-L366

Added lines #L362 - L366 were not covered by tests

insertAttachmentFile(filePath: string) {
if (!this.hasActiveConnection()) {
throw new Error('Not connected to server.')
}
return this.sessionConnection.insertAttachmentFile(filePath)
}

Check warning on line 373 in src/services/SyncService.ts

View check run for this annotation

Codecov / codecov/patch

src/services/SyncService.ts#L369-L373

Added lines #L369 - L373 were not covered by tests

createAttachment(template: object) {
if (!this.hasActiveConnection()) {
throw new Error('Not connected to server.')
}
return this.sessionConnection.createAttachment(template)
}

Check warning on line 380 in src/services/SyncService.ts

View check run for this annotation

Codecov / codecov/patch

src/services/SyncService.ts#L376-L380

Added lines #L376 - L380 were not covered by tests

// For better typing use the bus directly: `syncService.bus.on()`.
on(event: keyof EventTypes, callback: Handler<unknown>) {
Expand All @@ -389,9 +386,9 @@
}

off(event: keyof EventTypes, callback: Handler<unknown>) {
this.bus.off(event, callback)
return this
}

Check warning on line 391 in src/services/SyncService.ts

View check run for this annotation

Codecov / codecov/patch

src/services/SyncService.ts#L389-L391

Added lines #L389 - L391 were not covered by tests

emit(event: keyof EventTypes, data?: unknown) {
this.bus.emit(event, data)
Expand Down
Loading