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
+13
-2
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/sandrodach/mindsift/internal/auth"
|
||||
"github.com/sandrodach/mindsift/internal/collab"
|
||||
"github.com/sandrodach/mindsift/internal/config"
|
||||
"github.com/sandrodach/mindsift/internal/domain"
|
||||
"github.com/sandrodach/mindsift/internal/storage"
|
||||
@@ -29,6 +30,9 @@ type Server struct {
|
||||
store *storage.Store
|
||||
wsHub *ws.Hub
|
||||
staticFS fs.FS
|
||||
|
||||
leases *collab.LeaseManager // block leases and open sessions of notes
|
||||
notifier *collab.Notifier // wakes long-poll requests on note changes
|
||||
}
|
||||
|
||||
func NewServer(cfg *config.Config, store *storage.Store, wsHub *ws.Hub, staticFS fs.FS) *Server {
|
||||
@@ -37,6 +41,8 @@ func NewServer(cfg *config.Config, store *storage.Store, wsHub *ws.Hub, staticFS
|
||||
store: store,
|
||||
wsHub: wsHub,
|
||||
staticFS: staticFS,
|
||||
leases: collab.NewLeaseManager(nil),
|
||||
notifier: collab.NewNotifier(),
|
||||
}
|
||||
wsHub.SetRoomAuthorizer(srv.canJoinRoom)
|
||||
go srv.startAlarmTicker()
|
||||
@@ -46,8 +52,7 @@ func NewServer(cfg *config.Config, store *storage.Store, wsHub *ws.Hub, staticFS
|
||||
// canJoinRoom allows realtime rooms only to users with read access: "doc:<id>" for notes, otherwise a shared list ID.
|
||||
func (s *Server) canJoinRoom(userID, room string) bool {
|
||||
if docID, ok := strings.CutPrefix(room, "doc:"); ok {
|
||||
_, err := s.store.GetDocumentByID(userID, docID)
|
||||
return err == nil
|
||||
return s.store.DocRole(userID, docID) != ""
|
||||
}
|
||||
_, err := s.store.GetListByID(userID, room)
|
||||
return err == nil
|
||||
@@ -159,6 +164,12 @@ func (s *Server) Router() http.Handler {
|
||||
r.Delete("/api/v1/documents/{id}", s.handleDeleteDocument)
|
||||
r.Post("/api/v1/documents/{id}/blocks/{blockId}/enhanced-task", s.handleCreateEnhancedTask)
|
||||
|
||||
// Collaborative note editing (operation log)
|
||||
r.Get("/api/v1/docs/{id}/snapshot", s.handleDocSnapshot)
|
||||
r.Post("/api/v1/docs/{id}/sync", s.handleDocSync)
|
||||
r.Get("/api/v1/docs/{id}/changes", s.handleDocChanges)
|
||||
r.Post("/api/v1/docs/{id}/leave", s.handleDocLeave)
|
||||
|
||||
// Document Sharing & Collaboration
|
||||
r.Get("/api/v1/documents/{id}/shares", s.handleGetDocShares)
|
||||
r.Post("/api/v1/documents/{id}/shares", s.handleCreateDocShare)
|
||||
|
||||
Reference in new issue
Block a user