diff --git a/internal/docrevision/ids.go b/internal/docrevision/ids.go new file mode 100644 index 00000000..32c72b82 --- /dev/null +++ b/internal/docrevision/ids.go @@ -0,0 +1,103 @@ +package docrevision + +// advanceID 把进程内分配计数推进到不少于已落库 ID,避免下一临时 ID 撞上真实对象。 +func advanceID(counter *int64, id int64) { + if counter == nil || id <= 0 { + return + } + if id > *counter { + *counter = id + } +} + +// nextFreeID 分配一个当前 map 中未被占用的临时 ID。 +func nextFreeID(counter *int64, occupied func(int64) bool) int64 { + if counter == nil { + return 0 + } + for { + *counter++ + id := *counter + if occupied == nil || !occupied(id) { + return id + } + } +} + +func (s *Service) adoptSnapshotID(snap *Snapshot, dbID int64) { + if s == nil || snap == nil || dbID <= 0 { + return + } + oldID := snap.ID + if oldID != 0 && oldID != dbID { + if existing := s.snapshots[oldID]; existing == snap { + delete(s.snapshots, oldID) + } + } + snap.ID = dbID + s.snapshots[dbID] = snap + advanceID(&s.nextSnapID, dbID) +} + +func (s *Service) adoptVersionID(ver *Version, dbID int64) { + if s == nil || ver == nil || dbID <= 0 { + return + } + oldID := ver.ID + if oldID != 0 && oldID != dbID { + if existing := s.versions[oldID]; existing == ver { + delete(s.versions, oldID) + } + } + ver.ID = dbID + s.versions[dbID] = ver + advanceID(&s.nextVerID, dbID) +} + +func (s *Service) adoptEventID(ev *ChangeEvent, dbID int64) { + if s == nil || ev == nil || dbID <= 0 { + return + } + oldID := ev.ID + if oldID != 0 && oldID != dbID { + if existing := s.events[oldID]; existing == ev { + delete(s.events, oldID) + } + } + ev.ID = dbID + s.events[dbID] = ev + if ev.IdempotencyKey != "" { + s.idemp[ev.IdempotencyKey] = dbID + } + advanceID(&s.nextEvtID, dbID) +} + +func (s *Service) adoptReportID(rep *ChangeReport, dbID int64) { + if s == nil || rep == nil || dbID <= 0 { + return + } + oldID := rep.ID + if oldID != 0 && oldID != dbID { + if existing := s.reports[oldID]; existing == rep { + delete(s.reports, oldID) + } + } + rep.ID = dbID + s.reports[dbID] = rep + advanceID(&s.nextRepID, dbID) +} + +func (s *Service) adoptCandidateID(c *AgentCandidate, dbID int64) { + if s == nil || c == nil || dbID <= 0 { + return + } + oldID := c.ID + if oldID != 0 && oldID != dbID { + if existing := s.candidates[oldID]; existing == c { + delete(s.candidates, oldID) + } + } + c.ID = dbID + s.candidates[dbID] = c + advanceID(&s.nextCandID, dbID) +} diff --git a/internal/docrevision/ids_test.go b/internal/docrevision/ids_test.go new file mode 100644 index 00000000..f979290a --- /dev/null +++ b/internal/docrevision/ids_test.go @@ -0,0 +1,70 @@ +package docrevision + +import ( + "testing" + "time" +) + +func TestNextFreeID_SkipsOccupied(t *testing.T) { + var counter int64 = 248 + occupied := map[int64]bool{249: true} + got := nextFreeID(&counter, func(id int64) bool { return occupied[id] }) + if got != 250 { + t.Fatalf("next free id=%d want 250", got) + } +} + +func TestAdoptSnapshotID_DoesNotDeleteOtherObject(t *testing.T) { + s := NewService(nil) + baseline := &Snapshot{ID: 249, DocID: "doc_a", ContentSHA256: "aa", CreatedAt: time.Now().UTC()} + temp := &Snapshot{ID: 249, DocID: "doc_b", ContentSHA256: "bb", CreatedAt: time.Now().UTC()} + s.snapshots[249] = baseline + s.nextSnapID = 248 + + s.adoptSnapshotID(temp, 250) + if s.snapshots[249] != baseline { + t.Fatal("adopting a different object must not delete the real baseline") + } + if s.snapshots[250] != temp || temp.ID != 250 { + t.Fatalf("temp must live at 250, id=%d", temp.ID) + } + if s.nextSnapID != 250 { + t.Fatalf("counter=%d want 250", s.nextSnapID) + } +} + +func TestAdoptSnapshotID_RemovesOnlySameObject(t *testing.T) { + s := NewService(nil) + snap := &Snapshot{ID: 1, DocID: "doc_a", ContentSHA256: "aa"} + s.snapshots[1] = snap + s.adoptSnapshotID(snap, 249) + if _, ok := s.snapshots[1]; ok { + t.Fatal("same-object remap must drop the old temp key") + } + if s.snapshots[249] != snap || snap.ID != 249 { + t.Fatal("same object must occupy the database id") + } + if s.nextSnapID < 249 { + t.Fatalf("counter must advance to db id, got %d", s.nextSnapID) + } +} + +func TestPutSnapshotLocked_DoesNotClobberExistingBaseline(t *testing.T) { + s := NewService(nil) + s.mu.Lock() + defer s.mu.Unlock() + baseline := &Snapshot{ID: 249, DocID: "doc_seed", ContentSHA256: "base"} + s.snapshots[249] = baseline + s.nextSnapID = 248 + s.blobs["out"] = &ContentBlob{ContentSHA256: "out", ContentMarkdown: "# out\n"} + got := s.putSnapshotLocked("doc_new", "# out\n", "out", nil, SourceAgent, nil, "run", ActorSnapshot{}, "run", nil) + if got.ID == 249 { + t.Fatal("temporary snapshot must not reuse the occupied baseline id") + } + if s.snapshots[249] != baseline { + t.Fatal("baseline snapshot must remain") + } + if s.snapshots[got.ID] != got { + t.Fatal("new snapshot must be stored under its own id") + } +} diff --git a/internal/docrevision/pg_id_remap_test.go b/internal/docrevision/pg_id_remap_test.go new file mode 100644 index 00000000..802264fa --- /dev/null +++ b/internal/docrevision/pg_id_remap_test.go @@ -0,0 +1,198 @@ +package docrevision_test + +import ( + "context" + "fmt" + "testing" + "time" + + "chat-agent-system/internal/db" + "chat-agent-system/internal/docrevision" + + "github.com/jackc/pgx/v5/pgxpool" +) + +func TestMaterializeAgentOutput_HighSequenceFirstGenKeepsBaseline_PG(t *testing.T) { + if testing.Short() { + t.Skip("skipping PostgreSQL id-remap integration in -short") + } + dsn := db.SkipIfNoTestDatabaseURL(t) + ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second) + defer cancel() + pool, err := pgxpool.New(ctx, dsn) + if err != nil { + t.Skipf("postgres unavailable: %v", err) + } + defer pool.Close() + if err := pool.Ping(ctx); err != nil { + t.Skipf("postgres ping failed: %v", err) + } + if err := db.RunMigrationsOnceForTest(ctx, pool, dsn); err != nil { + t.Fatalf("migration failed: %v", err) + } + var n int + if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM information_schema.tables WHERE table_name='document_snapshots'`).Scan(&n); err != nil || n == 0 { + t.Skip("document_snapshots table missing") + } + + suffix := time.Now().UnixNano() + seedDoc := fmt.Sprintf("doc_idremap_seed_%d", suffix) + genDoc := fmt.Sprintf("doc_idremap_gen_%d", suffix) + t.Cleanup(func() { + cleanupRevisionDocs(context.Background(), pool, seedDoc, genDoc) + }) + cleanupRevisionDocs(ctx, pool, seedDoc, genDoc) + + seedSnap := nextExplicitID(t, ctx, pool, "document_snapshots") + seedVer := nextExplicitID(t, ctx, pool, "document_versions") + seedEvt := nextExplicitID(t, ctx, pool, "document_change_events") + seedHighIDs(t, ctx, pool, seedDoc, seedSnap, seedVer, seedEvt) + + svc := docrevision.NewService(pool) + seedState, err := svc.GetRevisionState(ctx, seedDoc) + if err != nil { + t.Fatalf("hydrate seed: %v", err) + } + if seedState.BaselineSnapshotID != seedSnap { + t.Fatalf("seed baseline=%d want %d", seedState.BaselineSnapshotID, seedSnap) + } + + outMD := "# 首轮交付\n\n## 范围\n\nPG 序列碰撞回归正文\n" + res, err := svc.MaterializeAgentOutput(ctx, docrevision.AgentMaterializeRequest{ + DocID: genDoc, RunID: fmt.Sprintf("run-idremap-%d", suffix), OutputMarkdown: outMD, + ProjectID: 1, WorkDir: "ws/idremap", + ActorKind: docrevision.ActorAgentSquad, + ActorSnapshot: docrevision.ActorSnapshot{SquadName: "bizdev-squad", Model: "test"}, + AutoCommitUserID: 42, + }) + if err != nil { + t.Fatalf("materialize first gen: %v", err) + } + if res.State == nil || res.State.CurrentVersionNo != 2 { + t.Fatalf("want V2 after auto-commit, got %+v", res.State) + } + if res.Candidate == nil || res.Candidate.Status != docrevision.CandidateCommitted { + t.Fatalf("candidate must be committed, got %+v", res.Candidate) + } + if res.State.BaselineSnapshotID == 0 { + t.Fatal("baseline snapshot id must remain") + } + versions, err := svc.ListVersions(ctx, genDoc) + if err != nil { + t.Fatal(err) + } + var v1, v2 *docrevision.Version + for i := range versions { + switch versions[i].VersionNo { + case 1: + v1 = &versions[i] + case 2: + v2 = &versions[i] + } + } + if v1 == nil || v2 == nil { + t.Fatalf("want V1+V2, got %+v", versions) + } + if v1.BaselineSnapshotID != seedSnap+1 { + t.Fatalf("first-gen template snapshot want %d (next after seed %d), got %d", seedSnap+1, seedSnap, v1.BaselineSnapshotID) + } + if v2.BaselineSnapshotID == v1.BaselineSnapshotID || v2.HeadSnapshotID == v1.BaselineSnapshotID { + t.Fatalf("V2 must not reuse/drop V1 baseline: v1=%+v v2=%+v", v1, v2) + } + if res.Candidate.InputSnapshotID != v1.BaselineSnapshotID { + t.Fatalf("candidate input=%d want V1 baseline %d", res.Candidate.InputSnapshotID, v1.BaselineSnapshotID) + } + + seedAfter, err := svc.GetRevisionState(ctx, seedDoc) + if err != nil { + t.Fatal(err) + } + if seedAfter.BaselineSnapshotID != seedSnap || seedAfter.HeadSnapshotID != seedSnap { + t.Fatalf("seed doc must keep snapshot %d, got %+v", seedSnap, seedAfter) + } + + restart := docrevision.NewService(pool) + st2, err := restart.GetRevisionState(ctx, genDoc) + if err != nil { + t.Fatalf("hydrate gen after new Service: %v", err) + } + if st2.CurrentVersionNo != 2 || st2.HeadSnapshotID == 0 { + t.Fatalf("restart hydrate want V2/head, got %+v", st2) + } + cand, err := restart.GetCandidate(ctx, genDoc, res.Candidate.ID) + if err != nil || cand == nil || cand.Status != docrevision.CandidateCommitted { + t.Fatalf("restart candidate: %+v err=%v", cand, err) + } + if st2.BaselineSnapshotID == 0 { + t.Fatal("restart must keep baseline") + } + + seedRestart, err := restart.GetRevisionState(ctx, seedDoc) + if err != nil { + t.Fatal(err) + } + if seedRestart.BaselineSnapshotID != seedSnap { + t.Fatalf("restart seed baseline=%d", seedRestart.BaselineSnapshotID) + } +} + +func nextExplicitID(t *testing.T, ctx context.Context, pool *pgxpool.Pool, table string) int64 { + t.Helper() + var maxID int64 + q := fmt.Sprintf(`SELECT COALESCE(MAX(id), 0) FROM %s`, table) + if err := pool.QueryRow(ctx, q).Scan(&maxID); err != nil { + t.Fatalf("max id %s: %v", table, err) + } + return maxID + 50 +} + +func seedHighIDs(t *testing.T, ctx context.Context, pool *pgxpool.Pool, docID string, snapID, verID, evtID int64) { + t.Helper() + sha := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + if _, err := pool.Exec(ctx, ` + INSERT INTO document_content_blobs (content_sha256, content_markdown, byte_size) + VALUES ($1, $2, 8) + ON CONFLICT (content_sha256) DO NOTHING + `, sha, "# seed\n"); err != nil { + t.Fatalf("seed blob: %v", err) + } + if _, err := pool.Exec(ctx, ` + INSERT INTO document_snapshots (id, doc_id, content_sha256, source_kind) + VALUES ($1, $2, $3, 'template') + `, snapID, docID, sha); err != nil { + t.Fatalf("seed snapshot: %v", err) + } + if _, err := pool.Exec(ctx, ` + INSERT INTO document_versions (id, doc_id, version_no, status, origin_kind, baseline_snapshot_id, head_snapshot_id) + VALUES ($1, $2, 1, 'open', 'template', $3, $3) + `, verID, docID, snapID); err != nil { + t.Fatalf("seed version: %v", err) + } + if _, err := pool.Exec(ctx, ` + INSERT INTO document_change_events ( + id, doc_id, version_id, event_no, to_snapshot_id, change_kind, actor_kind, idempotency_key + ) VALUES ($1, $2, $3, 1, $4, 'import', 'template', $5) + `, evtID, docID, verID, snapID, "seed:"+docID); err != nil { + t.Fatalf("seed event: %v", err) + } + if _, err := pool.Exec(ctx, `SELECT setval('document_snapshots_id_seq', $1, true)`, snapID); err != nil { + t.Fatalf("setval snapshots: %v", err) + } + if _, err := pool.Exec(ctx, `SELECT setval('document_versions_id_seq', $1, true)`, verID); err != nil { + t.Fatalf("setval versions: %v", err) + } + if _, err := pool.Exec(ctx, `SELECT setval('document_change_events_id_seq', $1, true)`, evtID); err != nil { + t.Fatalf("setval events: %v", err) + } +} + +func cleanupRevisionDocs(ctx context.Context, pool *pgxpool.Pool, docIDs ...string) { + for _, docID := range docIDs { + _, _ = pool.Exec(ctx, `DELETE FROM document_agent_candidates WHERE doc_id=$1`, docID) + _, _ = pool.Exec(ctx, `UPDATE document_versions SET change_report_id=NULL WHERE doc_id=$1`, docID) + _, _ = pool.Exec(ctx, `DELETE FROM document_change_reports WHERE doc_id=$1`, docID) + _, _ = pool.Exec(ctx, `DELETE FROM document_change_events WHERE doc_id=$1`, docID) + _, _ = pool.Exec(ctx, `DELETE FROM document_versions WHERE doc_id=$1`, docID) + _, _ = pool.Exec(ctx, `DELETE FROM document_snapshots WHERE doc_id=$1`, docID) + } +} diff --git a/internal/docrevision/pg_mutate.go b/internal/docrevision/pg_mutate.go index 24be7374..28d49f50 100644 --- a/internal/docrevision/pg_mutate.go +++ b/internal/docrevision/pg_mutate.go @@ -26,8 +26,7 @@ func (s *Service) openTemplatePGLocked(ctx context.Context, docID, canon, sha st if err != nil { return err } - sn.ID = id - s.snapshots[id] = sn + s.adoptSnapshotID(sn, id) ver := &Version{ DocID: docID, VersionNo: 1, Status: VersionOpen, OriginKind: OriginTemplate, @@ -37,8 +36,7 @@ func (s *Service) openTemplatePGLocked(ctx context.Context, docID, canon, sha st if err != nil { return err } - ver.ID = vid - s.versions[vid] = ver + s.adoptVersionID(ver, vid) ev := &ChangeEvent{ DocID: docID, VersionID: vid, EventNo: 1, ToSnapshotID: id, @@ -50,9 +48,7 @@ func (s *Service) openTemplatePGLocked(ctx context.Context, docID, canon, sha st if err != nil { return err } - ev.ID = eid - s.events[eid] = ev - s.idemp[ev.IdempotencyKey] = eid + s.adoptEventID(ev, eid) root := &DocRootPointers{ DocID: docID, Kind: "controlled", ProjectID: projectID, WorkDir: workDir, @@ -84,30 +80,20 @@ func (s *Service) persistHumanRevisionLocked(ctx context.Context, root *DocRootP if err := s.ensureBlobTX(ctx, tx, blob); err != nil { return err } - oldSnapID := snap.ID id, err := s.insertSnapshotTX(ctx, tx, snap) if err != nil { return err } - if oldSnapID != 0 && oldSnapID != id { - delete(s.snapshots, oldSnapID) - } - snap.ID = id - s.snapshots[id] = snap + s.adoptSnapshotID(snap, id) if isBaseline { ver.BaselineSnapshotID = id ver.HeadSnapshotID = id - oldVerID := ver.ID vid, err := s.insertVersionTX(ctx, tx, ver) if err != nil { return err } - if oldVerID != 0 && oldVerID != vid { - delete(s.versions, oldVerID) - } - ver.ID = vid - s.versions[vid] = ver + s.adoptVersionID(ver, vid) root.LedgerVersionID = &vid root.HeadSnapshotID = &id ev.VersionID = vid @@ -122,19 +108,11 @@ func (s *Service) persistHumanRevisionLocked(ctx context.Context, root *DocRootP ev.VersionID = ver.ID } - oldEv := ev.ID eid, err := s.insertEventTX(ctx, tx, ev) if err != nil { return err } - if oldEv != 0 && oldEv != eid { - delete(s.events, oldEv) - } - ev.ID = eid - s.events[eid] = ev - if ev.IdempotencyKey != "" { - s.idemp[ev.IdempotencyKey] = eid - } + s.adoptEventID(ev, eid) return s.projectRootPointersTX(ctx, tx, root) }) } @@ -151,41 +129,26 @@ func (s *Service) persistAgentCandidateLocked(ctx context.Context, outSnap *Snap if err := s.ensureBlobTX(ctx, tx, blob); err != nil { return err } - oldSnap := outSnap.ID sid, err := s.insertSnapshotTX(ctx, tx, outSnap) if err != nil { return err } - if oldSnap != 0 && oldSnap != sid { - delete(s.snapshots, oldSnap) - } - outSnap.ID = sid - s.snapshots[sid] = outSnap - c.OutputSnapshotID = &sid + s.adoptSnapshotID(outSnap, sid) + c.OutputSnapshotID = &outSnap.ID rep.ToSnapshotID = sid - oldRep := rep.ID rid, err := s.insertReportTX(ctx, tx, rep) if err != nil { return err } - if oldRep != 0 && oldRep != rid { - delete(s.reports, oldRep) - } - rep.ID = rid - s.reports[rid] = rep - c.ChangeReportID = &rid + s.adoptReportID(rep, rid) + c.ChangeReportID = &rep.ID - oldC := c.ID cid, err := s.insertCandidateTX(ctx, tx, c) if err != nil { return err } - if oldC != 0 && oldC != cid { - delete(s.candidates, oldC) - } - c.ID = cid - s.candidates[cid] = c + s.adoptCandidateID(c, cid) return nil }) } @@ -201,32 +164,22 @@ func (s *Service) persistAgentCommitLocked(ctx context.Context, root *DocRootPoi if err := s.updateVersionTX(ctx, tx, oldVer); err != nil { return err } - oldVerID := newVer.ID vid, err := s.insertVersionTX(ctx, tx, newVer) if err != nil { return err } - if oldVerID != 0 && oldVerID != vid { - delete(s.versions, oldVerID) - } - newVer.ID = vid - s.versions[vid] = newVer - root.LedgerVersionID = &vid + s.adoptVersionID(newVer, vid) + root.LedgerVersionID = &newVer.ID hid := outSnap.ID root.HeadSnapshotID = &hid ev.VersionID = vid ev.ToSnapshotID = outSnap.ID - oldEv := ev.ID eid, err := s.insertEventTX(ctx, tx, ev) if err != nil { return err } - if oldEv != 0 { - delete(s.events, oldEv) - } - ev.ID = eid - s.events[eid] = ev + s.adoptEventID(ev, eid) if err := s.updateCandidateTX(ctx, tx, c); err != nil { return err diff --git a/internal/docrevision/pg_store.go b/internal/docrevision/pg_store.go index b0157f97..5541dbdc 100644 --- a/internal/docrevision/pg_store.go +++ b/internal/docrevision/pg_store.go @@ -457,15 +457,3 @@ func (s *Service) withTx(ctx context.Context, fn func(pgx.Tx) error) error { // Pool exposes the underlying pool for integration tests. func (s *Service) Pool() *pgxpool.Pool { return s.pool } - -// remapSnapshotID updates parent pointers after a snapshot receives its real PG id. -func (s *Service) remapSnapshotID(oldID, newID int64) { - if oldID == 0 || oldID == newID { - return - } - if sn := s.snapshots[oldID]; sn != nil { - delete(s.snapshots, oldID) - sn.ID = newID - s.snapshots[newID] = sn - } -} diff --git a/internal/docrevision/service.go b/internal/docrevision/service.go index e0c6597a..ce5c46d4 100644 --- a/internal/docrevision/service.go +++ b/internal/docrevision/service.go @@ -499,17 +499,15 @@ func (s *Service) CreateAgentCandidate(ctx context.Context, req AgentCandidateRe if *root.HeadSnapshotID != req.InputSnapshotID { status = CandidateStale } - s.nextRepID++ - rep.ID = s.nextRepID + rep.ID = nextFreeID(&s.nextRepID, func(id int64) bool { return s.reports[id] != nil }) rep.DocID = req.DocID rep.FromSnapshotID = req.InputSnapshotID rep.ToSnapshotID = outSnap.ID rep.CreatedAt = time.Now().UTC() s.reports[rep.ID] = rep - s.nextCandID++ c := &AgentCandidate{ - ID: s.nextCandID, DocID: req.DocID, InputSnapshotID: req.InputSnapshotID, + ID: nextFreeID(&s.nextCandID, func(id int64) bool { return s.candidates[id] != nil }), DocID: req.DocID, InputSnapshotID: req.InputSnapshotID, OutputSnapshotID: &outSnap.ID, RunID: req.RunID, ProposalID: req.ProposalID, Status: status, AuthorizedScope: req.AuthorizedScope, ChangeReportID: &rep.ID, ActorKind: req.ActorKind, ActorSnapshot: req.ActorSnapshot, CreatedAt: time.Now().UTC(), @@ -621,9 +619,11 @@ func (s *Service) CommitAgentVersion(ctx context.Context, docID string, candidat c.DecidedAt = &now if s.usingPG() { if err := s.withTx(ctx, func(tx pgx.Tx) error { - if _, err := s.insertEventTX(ctx, tx, ev); err != nil { + eid, err := s.insertEventTX(ctx, tx, ev) + if err != nil { return err } + s.adoptEventID(ev, eid) return s.updateCandidateTX(ctx, tx, c) }); err != nil { s.forgetDocLocked(docID) @@ -860,8 +860,7 @@ func (s *Service) ImportFreeformLegacy(ctx context.Context, docID string, ownerI ID: 0, DocID: docID, VersionNo: startNo, Status: VersionOpen, OriginKind: OriginMigration, BaselineSnapshotID: snap.ID, HeadSnapshotID: snap.ID, OpenedAt: time.Now().UTC(), } - s.nextVerID++ - ver.ID = s.nextVerID + ver.ID = nextFreeID(&s.nextVerID, func(id int64) bool { return s.versions[id] != nil }) s.versions[ver.ID] = ver root := &DocRootPointers{ DocID: docID, Kind: "freeform", ProjectID: projectID, @@ -939,9 +938,8 @@ func (s *Service) putSnapshotLocked(docID, canon, sha string, parent *int64, sou } else { blocks = BuildBlockMap(canon, clientBlocks) } - s.nextSnapID++ snap := &Snapshot{ - ID: s.nextSnapID, DocID: docID, ContentSHA256: sha, ParentSnapshotID: parent, + ID: nextFreeID(&s.nextSnapID, func(id int64) bool { return s.snapshots[id] != nil }), DocID: docID, ContentSHA256: sha, ParentSnapshotID: parent, BlockMap: blocks, SourceKind: source, ActorUserID: userID, ActorRef: actorRef, ActorSnapshot: actor, SourceRunID: runID, CreatedAt: time.Now().UTC(), } @@ -961,9 +959,8 @@ func (s *Service) openVersionLocked(docID, origin string, baselineID, headID int v.SealedAt = &now } } - s.nextVerID++ ver := &Version{ - ID: s.nextVerID, DocID: docID, VersionNo: maxNo + 1, Status: VersionOpen, + ID: nextFreeID(&s.nextVerID, func(id int64) bool { return s.versions[id] != nil }), DocID: docID, VersionNo: maxNo + 1, Status: VersionOpen, OriginKind: origin, BaselineSnapshotID: baselineID, HeadSnapshotID: headID, SourceRunID: runID, OpenedAt: time.Now().UTC(), } @@ -972,9 +969,8 @@ func (s *Service) openVersionLocked(docID, origin string, baselineID, headID int } func (s *Service) putEventLocked(docID string, versionID int64, eventNo int, from *int64, to int64, kind, actorKind string, userID *int64, actorRef string, actor ActorSnapshot, hunks []DiffHunk, editSession, requestID, idemp string) *ChangeEvent { - s.nextEvtID++ ev := &ChangeEvent{ - ID: s.nextEvtID, DocID: docID, VersionID: versionID, EventNo: eventNo, + ID: nextFreeID(&s.nextEvtID, func(id int64) bool { return s.events[id] != nil }), DocID: docID, VersionID: versionID, EventNo: eventNo, FromSnapshotID: from, ToSnapshotID: to, ChangeKind: kind, ActorKind: actorKind, ActorUserID: userID, ActorRef: actorRef, ActorSnapshot: actor, ChangeSet: hunks, EditSessionID: editSession, RequestID: requestID, IdempotencyKey: idemp, CreatedAt: time.Now().UTC(),