Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
f471879
wip: try y-indexeddb
max-nextcloud Jun 13, 2025
c4e4829
chore(split) useIndexedDbProvider from Editor.vue
max-nextcloud Sep 4, 2025
44af333
fix(cron): do not reset document
max-nextcloud Sep 4, 2025
dd8e83a
enh(yjs): store baseVersionEtag alongside doc
max-nextcloud Sep 4, 2025
1838b4b
fix(offline): persist dirty state in indexed db
max-nextcloud Oct 14, 2025
5b8bd7a
chore(test): explore empty changesets
max-nextcloud Oct 22, 2025
9c28542
chore(rename): use privateMethods for emitError and emitDocumentState…
max-nextcloud Oct 26, 2025
6e1a570
chore(cleanup): _getContent alias for serialize
max-nextcloud Oct 26, 2025
1daa1e7
chore(refactor): handle open data in websocket polyfill
max-nextcloud Oct 26, 2025
cf1c4ee
fix(sync): only accept sync protocol and return sync step 2
max-nextcloud Oct 26, 2025
48d4a25
enh(sync): recover automatically from outdated / renamed doc
max-nextcloud Oct 27, 2025
9044b2c
fix(sync): ensure dirty is updated when saving in onDestroy
max-nextcloud Nov 4, 2025
855ad42
chore(logging): some optional debug logging
max-nextcloud Nov 5, 2025
e444f5c
fix(sync): actually disable browser broadcast
max-nextcloud Nov 5, 2025
ac55c34
chore(test): conflict and sync with autoreload
max-nextcloud Nov 5, 2025
f1336d6
chore(type) a few more files
max-nextcloud Nov 6, 2025
f003e1e
chore(test): add initial test for indexed db
max-nextcloud Nov 7, 2025
2557518
fix(sync): Cleanup sessions even with unsaved changes
max-nextcloud Nov 7, 2025
ca44281
chore(type): EditorFactory and setInitialYjsState
max-nextcloud Nov 10, 2025
e309a1c
enh(sync): updateFromContent()
max-nextcloud Nov 10, 2025
7176762
fix(indexedDB): handle conflict with local change
max-nextcloud Jan 7, 2026
558b0b5
chore(log): debug log when attempting to open sync service twice
max-nextcloud Jan 7, 2026
a3df7af
fix(conflict): handle conflict when opening initial session
max-nextcloud Jan 7, 2026
b238823
fix(conflict): label buttons properly for unsaved local changes
max-nextcloud Jan 7, 2026
0dc6484
chore(split): reload handling local change in Editor.js
max-nextcloud Jan 14, 2026
e78bc47
fix(sync): make bc channel depend on base version etag
max-nextcloud Jan 16, 2026
fe1239b
fix(offline): save dirty file when opening
max-nextcloud Jan 16, 2026
51bdf94
fix(offline): autosave with disconnects
max-nextcloud Jan 19, 2026
63c3490
chore(spell): fix collision typo
max-nextcloud Mar 25, 2026
ecdaa57
chore(names): rename requireReconnect to displayConnectionIssue
max-nextcloud Mar 25, 2026
0068c00
chore(test): save and push offline changes on open
max-nextcloud Mar 26, 2026
e6e66ca
fix(sync): push steps from offline changes when opening file
max-nextcloud Mar 27, 2026
3accc5c
fix(sync): wait for indexeddb to load before reading dirty flag (ai c…
silverkszlo Apr 2, 2026
ae31553
fix(offline): wait for editor, idb and then save with manualSave=true…
silverkszlo Apr 2, 2026
d121297
fix(test): push first and then get content of file (ai code)
silverkszlo Apr 2, 2026
bad2a9a
fix(types): add missing mentionSearch parameter to createRichEditor
silverkszlo Apr 2, 2026
1f0ee35
chore(cleanup): remove obsolete imports after rebase
silverkszlo Apr 2, 2026
bd2c842
chore(lint): npm run prettier and lint
silverkszlo Apr 2, 2026
f2dbd63
fix(test): destroy editor after use
silverkszlo Apr 2, 2026
774a4fd
fix(Editor.js): forward slots to inner Editor component (ai code)
silverkszlo Apr 2, 2026
ecae811
fix(useIndexedDbProvider): don't write on closed connection
mejo- Apr 21, 2026
4c9cf3f
fix(conflict): persist local state in local storage
mejo- Apr 21, 2026
c036452
fix(documentStatus): display document status on conflict
mejo- Apr 21, 2026
27038fb
test(playwright): migrate conflict tests from Cypress
mejo- Apr 21, 2026
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
51 changes: 21 additions & 30 deletions cypress/e2e/api/SessionApi.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,34 +73,33 @@ describe('The session Api', function () {
cy.closeConnection(connection)
})

// Echoes all message types but queries
Object.entries(messages)
.filter(([key, _value]) => key !== 'query')
.forEach(([type, sample]) => {
it(`echos ${type} messages`, function () {
const steps = [sample]
const version = 0
cy.pushSteps({ connection, steps, version })
.its('version')
.should('eql', 0)
cy.syncSteps(connection)
.its('steps[0].data')
.should('eql', steps)
})
// Echoes updates and responses
;['update', 'response'].forEach((type) => {
it(`echos ${type} messages`, function () {
const steps = [messages[type]]
const version = 0
cy.pushSteps({ connection, steps, version })
.its('version')
.should('eql', 0)
cy.syncSteps(connection).its('steps[0].data').should('eql', steps)
})
})

it('responds to queries', function () {
it('responds to queries with updates and responses', function () {
const version = 0
Object.entries(messages).forEach(([type, sample]) => {
cy.pushSteps({ connection, steps: [sample], version })
})
cy.pushSteps({ connection, steps: [messages.query], version }).then(
(response) => {
cy.wrap(response).its('version').should('eql', 0)
cy.wrap(response).its('steps.length').should('eql', 1)
cy.wrap(response).its('steps.length').should('eql', 2)
cy.wrap(response)
.its('steps[0].data')
.should('eql', [messages.update])
cy.wrap(response)
.its('steps[1].data')
.should('eql', [messages.response])
},
)
})
Expand All @@ -111,7 +110,6 @@ describe('The session Api', function () {
let connection
let fileId
let filePath
let joining

beforeEach(function () {
cy.testName().then((name) => {
Expand Down Expand Up @@ -156,13 +154,10 @@ describe('The session Api', function () {
manualSave: true,
})
cy.openConnection({ fileId, filePath })
.then(({ connection: con, data }) => {
joining = con
return data
})
.its('documentState')
.as('joining')
.its('data.documentState')
.should('eql', documentState)
cy.closeConnection(joining)
cy.get('@joining').its('connection').then(cy.closeConnection)
})

afterEach(function () {
Expand All @@ -175,7 +170,6 @@ describe('The session Api', function () {
let connection
let filePath
let shareToken
let joining

beforeEach(function () {
cy.testName().then((name) => {
Expand Down Expand Up @@ -232,13 +226,10 @@ describe('The session Api', function () {
manualSave: true,
})
cy.openConnection({ filePath: '', token: shareToken })
.then(({ connection: con, data }) => {
joining = con
return data
})
.its('documentState')
.as('joining')
.its('data.documentState')
.should('eql', documentState)
cy.closeConnection(joining)
cy.get('@joining').its('connection').then(cy.closeConnection)
})
})

Expand Down
21 changes: 13 additions & 8 deletions cypress/e2e/api/SyncServiceProvider.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,20 @@ describe('Sync service provider', function () {
*/
function createProvider(ydoc) {
const relativePath = '.'
const { connection, openConnection, baseVersionEtag } = provideConnection({
fileId,
relativePath,
})
const { syncService } = provideSyncService(
connection,
openConnection,
baseVersionEtag,
let baseVersionEtag
const setBaseVersionEtag = (val) => {
baseVersionEtag = val
}
const getBaseVersionEtag = () => baseVersionEtag
const { connection, openConnection } = provideConnection(
{
fileId,
relativePath,
},
getBaseVersionEtag,
setBaseVersionEtag,
)
const { syncService } = provideSyncService(connection, openConnection)
const queue = []
syncService.bus.on('opened', () => syncService.startSync())
return createSyncServiceProvider({
Expand Down
180 changes: 0 additions & 180 deletions cypress/e2e/conflict.spec.js

This file was deleted.

22 changes: 8 additions & 14 deletions cypress/e2e/sync.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@ describe('Sync', () => {
'contain',
'The document could not be loaded.',
)
cy.intercept('**/apps/text/session/*/create').as('create')
cy.get('#editor-container .document-status').find('button').click()
// let first attempt fail
cy.wait('@create', { timeout: 10000 })
cy.get('#editor-container .document-status', { timeout: 30000 }).should(
'contain',
'The document could not be loaded.',
Expand All @@ -117,13 +120,13 @@ describe('Sync', () => {
cy.intercept('**/apps/text/session/*/*', (req) => {
req.continue()
}).as('alive')
cy.intercept('**/apps/text/session/*/create').as('create')
cy.get('#editor-container .document-status').find('button').click()
// this is the create request... - now with the alive alias
cy.wait('@alive', { timeout: 30000 })
cy.wait('@create', { timeout: 10000 })
.its('request.body')
.should('have.property', 'baseVersionEtag')
.should('not.be.empty')
cy.getContent().should('contain', 'Hello world')
})

it('recovers from a lost and closed connection', () => {
Expand Down Expand Up @@ -176,18 +179,9 @@ describe('Sync', () => {
cy.wait('@save')
cy.uploadTestFile('test.md')

cy.get('#editor-container .document-status', { timeout: 30000 }).should(
'contain',
'Editing session has expired.',
)

// Reload button works
cy.get('#editor-container .document-status button')
.contains('Reload')
.click()

cy.getContent()
cy.get('#editor-container .document-status .notecard').should('not.exist')
cy.getContent().should('not.exist')
cy.getContent().find('h2').should('contain', 'Hello world')
cy.getContent().find('li').should('not.exist') // was overwritten after the save
})

it('passes the doc content from one session to the next', () => {
Expand Down
5 changes: 0 additions & 5 deletions lib/Cron/Cleanup.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

namespace OCA\Text\Cron;

use OCA\Text\Exception\DocumentHasUnsavedChangesException;
use OCA\Text\Service\AttachmentService;
use OCA\Text\Service\DocumentService;
use OCA\Text\Service\SessionService;
Expand All @@ -37,10 +36,6 @@ public function __construct(
protected function run($argument): void {
$this->logger->debug('Run cleanup job for text documents');
foreach ($this->documentService->getAllWithNoActiveSession() as $document) {
try {
$this->documentService->resetDocument($document->getId());
} catch (DocumentHasUnsavedChangesException) {
}
$this->attachmentService->cleanupAttachments($document->getId());
}

Expand Down
4 changes: 2 additions & 2 deletions lib/Listeners/BeforeNodeWrittenListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ public function handle(Event $event): void {
}
// Reset document session to avoid manual conflict resolution if there's no unsaved steps
try {
$this->documentService->resetDocument($node->getId());
$this->documentService->resetDocument($node->getId(), true);
} catch (DocumentHasUnsavedChangesException|NotFoundException $e) {
// Do not throw during event handling in this is expected to happen
// DocumentHasUnsavedChangesException: A document editing session is likely ongoing, someone can resolve the conflict
// NotFoundException: The event was called oin a file that was just created so a NonExistingFile object is used that has no id yet
$this->logger->debug('Reset document skipped in BeforeNodeWrittenEvent', ['exception' => $e]);
$this->logger->warning('Reset document skipped in BeforeNodeWrittenEvent', ['exception' => $e]);
}
}
}
Loading
Loading