feat(notes): operation log, block leases and sync API for collaborative editing
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 50s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 50s
Phase 1 of the collaboration plan (server side):
- internal/collab: block tree with parent IDs and fractional order keys,
operations with conflict rules (version check, block leases, conflict
copies instead of lost text, restore on late edits, cycle checks),
in-memory leases/sessions and long-poll notifier.
- Storage: per-note lock + transaction, append-only effect log (doc_ops),
idempotent results per opId (doc_op_results), snapshot, log paging,
central DocRole. Old flat block data is converted to the tree.
- The existing whole-document save (PUT/PATCH) runs as an adapter through
the same engine, so the current editor keeps working and writes to the log.
- API: GET /docs/{id}/snapshot, POST /docs/{id}/sync, GET /docs/{id}/changes
(long poll), POST /docs/{id}/leave; DOC_SEQ poke for WebSocket/SSE rooms.
- Backend accepts editor (write) shares for notes and folders; the forced
read-only migration is gone. The share dialog still offers read only.
- Tests: conflict matrix, order keys, leases, storage adapter round trip,
idempotency, API protocol incl. long poll, and a randomized simulation
(4 clients, lost/duplicated requests and responses, offline phases,
lease expiry) checking convergence, no lost text and exactly-once results.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bcc06da3ee
commit
1ea0c78b41
23 files changed
+3266
-287
No files matched your search
@@ -0,0 +1,34 @@
|
||||
package collab
|
||||
|
||||
import "sync"
|
||||
|
||||
// Notifier wakes up long-poll requests waiting for changes of a document.
|
||||
type Notifier struct {
|
||||
mu sync.Mutex
|
||||
wait map[string]chan struct{}
|
||||
}
|
||||
|
||||
func NewNotifier() *Notifier {
|
||||
return &Notifier{wait: map[string]chan struct{}{}}
|
||||
}
|
||||
|
||||
// Changed returns a channel that is closed on the next Notify for docID.
|
||||
func (n *Notifier) Changed(docID string) <-chan struct{} {
|
||||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
ch, ok := n.wait[docID]
|
||||
if !ok {
|
||||
ch = make(chan struct{})
|
||||
n.wait[docID] = ch
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
func (n *Notifier) Notify(docID string) {
|
||||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
if ch, ok := n.wait[docID]; ok {
|
||||
close(ch)
|
||||
delete(n.wait, docID)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user