返回 DeepSeek-Reasonix
store.go
1 package workspacestate
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "os"
10 "path/filepath"
11 "slices"
12 "strings"
13 "sync"
14 "time"
15
16 "reasonix/internal/fileutil"
17 filelock "reasonix/internal/identitylock"
18 )
19
20 const (
21 SchemaVersion = 3
22 GlobalWorkspaceID = "global"
23 )
24
25 var (
26 ErrUnsupportedVersion = errors.New("workspace state version is unsupported")
27 ErrWorkspaceNotFound = errors.New("workspace is not registered")
28 ErrSessionNotFound = errors.New("session is not registered")
29 ErrMutationConflict = errors.New("workspace mutation conflicts with persisted state")
30 )
31
32 type Workspace struct {
33 Organization *Organization `json:"organization,omitempty"`
34 ID string `json:"id"`
35 Root string `json:"root"`
36 Title string `json:"title"`
37 SessionIDs []string `json:"sessionIds"`
38 Visible bool `json:"visible"`
39 CreatedAt time.Time `json:"createdAt"`
40 UpdatedAt time.Time `json:"updatedAt"`
41 extra map[string]json.RawMessage
42 }
43
44 type PendingCreate struct {
45 ParentSessionID string `json:"parentSessionId,omitempty"`
46 Presentation *Presentation `json:"presentation,omitempty"`
47 OperationID string `json:"operationId"`
48 WorkspaceID string `json:"workspaceId"`
49 SessionID string `json:"sessionId"`
50 CreatedAt time.Time `json:"createdAt"`
51 ArchiveSource string `json:"archiveSource,omitempty"`
52 extra map[string]json.RawMessage
53 }
54
55 type State struct {
56 Version int `json:"version"`
57 Generation uint64 `json:"generation"`
58 Initialized bool `json:"initialized"`
59 WorkspaceIDs []string `json:"workspaceIds"`
60 Workspaces map[string]Workspace `json:"workspaces"`
61 ArchivedSessionIDs []string `json:"archivedSessionIds"`
62 PendingCreates map[string]PendingCreate `json:"pendingCreates"`
63 SessionStates map[string]SessionState `json:"sessionStates"`
64 SourceMappings map[string]SourceMapping `json:"sourceMappings"`
65 PendingOperations map[string]Operation `json:"pendingOperations"`
66 RecoveryEntries map[string]RecoveryEntry `json:"recoveryEntries"`
67 Presentation map[string]Presentation `json:"presentation"`
68 extra map[string]json.RawMessage
69 }
70
71 type Store struct {
72 path string
73 mu sync.Mutex
74 beforeUpgrade func(context.Context) error
75 }
76
77 func NewStore(path string, beforeUpgrade ...func(context.Context) error) *Store {
78 store := &Store{path: filepath.Clean(path)}
79 if len(beforeUpgrade) > 0 {
80 store.beforeUpgrade = beforeUpgrade[0]
81 }
82 return store
83 }
84
85 func (s *Store) Path() string {
86 if s == nil {
87 return ""
88 }
89 return s.path
90 }
91
92 func (s *Store) Load(ctx context.Context) (State, error) {
93 if s == nil || strings.TrimSpace(s.path) == "" || s.path == "." {
94 return State{}, errors.New("workspace state path is required")
95 }
96 s.mu.Lock()
97 defer s.mu.Unlock()
98 if err := ctx.Err(); err != nil {
99 return State{}, err
100 }
101 state, err := load(s.path)
102 if err != nil {
103 return State{}, err
104 }
105 return cloneState(state)
106 }
107
108 func (s *Store) RenameWorkspace(ctx context.Context, workspaceID, title string) error {
109 return s.mutate(ctx, func(state *State) error {
110 workspace, ok := state.Workspaces[strings.TrimSpace(workspaceID)]
111 if !ok {
112 return ErrWorkspaceNotFound
113 }
114 workspace.Title = strings.TrimSpace(title)
115 workspace.UpdatedAt = time.Now().UTC()
116 state.Workspaces[workspace.ID] = workspace
117 return nil
118 })
119 }
120
121 func (s *Store) SetWorkspaceVisible(ctx context.Context, workspaceID string, visible bool) error {
122 return s.mutate(ctx, func(state *State) error {
123 workspace, ok := state.Workspaces[strings.TrimSpace(workspaceID)]
124 if !ok {
125 return ErrWorkspaceNotFound
126 }
127 workspace.Visible = visible
128 workspace.UpdatedAt = time.Now().UTC()
129 state.Workspaces[workspace.ID] = workspace
130 return nil
131 })
132 }
133
134 func (s *Store) MoveWorkspace(ctx context.Context, workspaceID, beforeWorkspaceID string) error {
135 workspaceID = strings.TrimSpace(workspaceID)
136 beforeWorkspaceID = strings.TrimSpace(beforeWorkspaceID)
137 return s.mutate(ctx, func(state *State) error {
138 if _, ok := state.Workspaces[workspaceID]; !ok {
139 return ErrWorkspaceNotFound
140 }
141 if beforeWorkspaceID != "" {
142 if _, ok := state.Workspaces[beforeWorkspaceID]; !ok {
143 return ErrWorkspaceNotFound
144 }
145 }
146 state.WorkspaceIDs = insertBefore(remove(state.WorkspaceIDs, workspaceID), workspaceID, beforeWorkspaceID)
147 return nil
148 })
149 }
150
151 func (s *Store) BeginCreate(ctx context.Context, pending PendingCreate) error {
152 pending.OperationID = strings.TrimSpace(pending.OperationID)
153 pending.WorkspaceID = strings.TrimSpace(pending.WorkspaceID)
154 pending.SessionID = strings.TrimSpace(pending.SessionID)
155 if pending.OperationID == "" || pending.WorkspaceID == "" || pending.SessionID == "" {
156 return errors.New("pending create requires operation, workspace, and session ids")
157 }
158 return s.mutate(ctx, func(state *State) error {
159 if _, ok := state.Workspaces[pending.WorkspaceID]; !ok {
160 return ErrWorkspaceNotFound
161 }
162 if state.SessionStates[pending.SessionID].Lifecycle == Deleted {
163 return ErrMutationConflict
164 }
165 if current, ok := state.PendingCreates[pending.SessionID]; ok {
166 if current.OperationID == pending.OperationID && current.WorkspaceID == pending.WorkspaceID {
167 return nil
168 }
169 return ErrMutationConflict
170 }
171 if owner, ok := sessionOwner(*state, pending.SessionID); ok && owner != pending.WorkspaceID {
172 return ErrMutationConflict
173 }
174 if pending.CreatedAt.IsZero() {
175 pending.CreatedAt = time.Now().UTC()
176 }
177 state.PendingCreates[pending.SessionID] = pending
178 return nil
179 })
180 }
181
182 func (s *Store) AttachSession(ctx context.Context, operationID, workspaceID, sessionID, beforeSessionID string) error {
183 return s.attachSession(ctx, operationID, workspaceID, sessionID, beforeSessionID, "", nil)
184 }
185
186 // AttachSessionFromSourceIfUnchanged publishes a derived child only while its
187 // resolved source is still active, in the same workspace, and at the same
188 // lifecycle generation.
189 func (s *Store) AttachSessionFromSourceIfUnchanged(
190 ctx context.Context,
191 operationID, workspaceID, sessionID, beforeSessionID, sourceSessionID string,
192 sourceGeneration uint64,
193 ) error {
194 return s.attachSession(ctx, operationID, workspaceID, sessionID, beforeSessionID, sourceSessionID, &sourceGeneration)
195 }
196
197 func (s *Store) attachSession(
198 ctx context.Context,
199 operationID, workspaceID, sessionID, beforeSessionID, sourceSessionID string,
200 sourceGeneration *uint64,
201 ) error {
202 operationID, workspaceID, sessionID = strings.TrimSpace(operationID), strings.TrimSpace(workspaceID), strings.TrimSpace(sessionID)
203 if workspaceID == "" || sessionID == "" {
204 return errors.New("attach requires workspace and session ids")
205 }
206 return s.mutate(ctx, func(state *State) error {
207 if sourceGeneration != nil {
208 sourceSessionID = strings.TrimSpace(sourceSessionID)
209 sourceState := state.SessionStates[sourceSessionID]
210 sourceOwner, owned := sessionOwner(*state, sourceSessionID)
211 if sourceSessionID == "" || !owned || sourceOwner != workspaceID ||
212 sourceState.Lifecycle != Active || sourceState.Generation != *sourceGeneration {
213 return ErrMutationConflict
214 }
215 }
216 if state.SessionStates[sessionID].Lifecycle == Deleted {
217 return ErrMutationConflict
218 }
219 workspace, ok := state.Workspaces[workspaceID]
220 if !ok {
221 return ErrWorkspaceNotFound
222 }
223 if owner, owned := sessionOwner(*state, sessionID); owned {
224 if owner != workspaceID {
225 return ErrMutationConflict
226 }
227 delete(state.PendingCreates, sessionID)
228 return nil
229 }
230 if operationID != "" {
231 pending, ok := state.PendingCreates[sessionID]
232 if !ok || pending.OperationID != operationID || pending.WorkspaceID != workspaceID {
233 return ErrMutationConflict
234 }
235 }
236 workspace.SessionIDs = insertBefore(workspace.SessionIDs, sessionID, beforeSessionID)
237 if sourceSessionID == "" {
238 sourceSessionID = state.PendingCreates[sessionID].ParentSessionID
239 }
240 attachOrganizationSession(&workspace, sessionID, sourceSessionID)
241 mirrorOrganizationOrder(&workspace)
242 if pending, ok := state.PendingCreates[sessionID]; ok && pending.Presentation != nil {
243 state.Presentation[sessionID] = *pending.Presentation
244 }
245 workspace.UpdatedAt = time.Now().UTC()
246 state.Workspaces[workspaceID] = workspace
247 delete(state.PendingCreates, sessionID)
248 return nil
249 })
250 }
251
252 // CommitRotation atomically publishes a prepared replacement into its
253 // workspace and, for Clear, archives the source without removing its stable
254 // position from the registry.
255 func (s *Store) CommitRotation(ctx context.Context, operationID, workspaceID, sessionID, beforeSessionID, archiveSessionID string) error {
256 operationID, workspaceID, sessionID = strings.TrimSpace(operationID), strings.TrimSpace(workspaceID), strings.TrimSpace(sessionID)
257 archiveSessionID = strings.TrimSpace(archiveSessionID)
258 if operationID == "" || workspaceID == "" || sessionID == "" {
259 return errors.New("rotation commit requires operation, workspace, and session ids")
260 }
261 return s.mutate(ctx, func(state *State) error {
262 workspace, ok := state.Workspaces[workspaceID]
263 if !ok {
264 return ErrWorkspaceNotFound
265 }
266 pending, ok := state.PendingCreates[sessionID]
267 if !ok || pending.OperationID != operationID || pending.WorkspaceID != workspaceID {
268 if owner, attached := sessionOwner(*state, sessionID); !attached || owner != workspaceID {
269 return ErrMutationConflict
270 }
271 } else if owner, attached := sessionOwner(*state, sessionID); attached && owner != workspaceID {
272 return ErrMutationConflict
273 } else if !attached {
274 workspace.SessionIDs = insertBefore(workspace.SessionIDs, sessionID, beforeSessionID)
275 attachOrganizationSession(&workspace, sessionID, "")
276 mirrorOrganizationOrder(&workspace)
277 workspace.UpdatedAt = time.Now().UTC()
278 state.Workspaces[workspaceID] = workspace
279 }
280 delete(state.PendingCreates, sessionID)
281 if archiveSessionID != "" {
282 if _, exists := sessionOwner(*state, archiveSessionID); !exists {
283 return ErrSessionNotFound
284 }
285 setLifecycle(state, archiveSessionID, Archived)
286 }
287 return nil
288 })
289 }
290
291 func (s *Store) AbortCreate(ctx context.Context, sessionID string) error {
292 return s.mutate(ctx, func(state *State) error {
293 delete(state.PendingCreates, strings.TrimSpace(sessionID))
294 return nil
295 })
296 }
297
298 // AbortCreateIfOperation removes only the caller's reservation. A late cleanup
299 // from an older draft operation must never erase a newer operation's claim.
300 func (s *Store) AbortCreateIfOperation(ctx context.Context, sessionID, operationID string) error {
301 sessionID, operationID = strings.TrimSpace(sessionID), strings.TrimSpace(operationID)
302 return s.mutate(ctx, func(state *State) error {
303 pending, ok := state.PendingCreates[sessionID]
304 if ok && pending.OperationID == operationID {
305 delete(state.PendingCreates, sessionID)
306 }
307 return nil
308 })
309 }
310
311 func (s *Store) MoveSession(ctx context.Context, workspaceID, sessionID, beforeSessionID string) error {
312 return s.moveSession(ctx, workspaceID, sessionID, beforeSessionID, nil)
313 }
314
315 // MoveSessionIfUnchanged reorders one active session only while the caller's
316 // resolved lifecycle generation and workspace owner remain current.
317 func (s *Store) MoveSessionIfUnchanged(ctx context.Context, workspaceID, sessionID, beforeSessionID string, generation uint64) error {
318 return s.moveSession(ctx, workspaceID, sessionID, beforeSessionID, &generation)
319 }
320
321 func (s *Store) moveSession(ctx context.Context, workspaceID, sessionID, beforeSessionID string, generation *uint64) error {
322 return s.mutate(ctx, func(state *State) error {
323 workspace, ok := state.Workspaces[strings.TrimSpace(workspaceID)]
324 if !ok {
325 return ErrWorkspaceNotFound
326 }
327 if !contains(workspace.SessionIDs, sessionID) {
328 return ErrSessionNotFound
329 }
330 status := state.SessionStates[sessionID]
331 if status.Lifecycle != Active {
332 return ErrSessionNotFound
333 }
334 if generation != nil && status.Generation != *generation {
335 return ErrMutationConflict
336 }
337 workspace.SessionIDs = insertBefore(remove(workspace.SessionIDs, sessionID), sessionID, beforeSessionID)
338 if o := workspace.Organization; o != nil {
339 key, before := SessionKey(sessionID), ""
340 if beforeSessionID != "" {
341 before = SessionKey(beforeSessionID)
342 }
343 o.Order = insertBefore(remove(o.Order, key), key, before)
344 o.ManualOrderEnabled = true
345 o.Revision++
346 mirrorOrganizationOrder(&workspace)
347 }
348 workspace.UpdatedAt = time.Now().UTC()
349 state.Workspaces[workspace.ID] = workspace
350 status.Generation++
351 state.SessionStates[sessionID] = status
352 return nil
353 })
354 }
355
356 func (s *Store) ArchiveSession(ctx context.Context, sessionID string) error {
357 return s.SetLifecycle(ctx, []string{sessionID}, Archived)
358 }
359
360 func (s *Store) RestoreSession(ctx context.Context, sessionID string) error {
361 return s.SetLifecycle(ctx, []string{sessionID}, Active)
362 }
363
364 func (s *Store) Contains(ctx context.Context, sessionID string) (bool, error) {
365 state, err := s.Load(ctx)
366 if err != nil {
367 return false, err
368 }
369 _, ok := sessionOwner(state, strings.TrimSpace(sessionID))
370 return ok, nil
371 }
372
373 // WithSessionUnchanged serializes a durable metadata commit with lifecycle and
374 // workspace changes, including writers in other processes. The callback must
375 // not call the registry; it may only commit session content metadata.
376 func (s *Store) WithSessionUnchanged(ctx context.Context, id, workspaceID string, generation uint64, commit func() error) error {
377 s.mu.Lock()
378 defer s.mu.Unlock()
379 release, err := filelock.Acquire(ctx, s.path+".lock")
380 if err != nil {
381 return err
382 }
383 defer release()
384 state, err := load(s.path)
385 if err != nil {
386 return err
387 }
388 owner, ok := sessionOwner(state, id)
389 status := state.SessionStates[id]
390 if !ok || status.Lifecycle != Active {
391 return ErrSessionNotFound
392 }
393 if owner != workspaceID || status.Generation != generation {
394 return ErrMutationConflict
395 }
396 return commit()
397 }
398
399 // WithStateLocked holds the registry's process and file locks while commit
400 // validates a read-only state snapshot and performs a related external write.
401 // The callback must not call this Store.
402 func (s *Store) WithStateLocked(ctx context.Context, commit func(State) error) error {
403 if s == nil || strings.TrimSpace(s.path) == "" || s.path == "." {
404 return errors.New("workspace state path is required")
405 }
406 s.mu.Lock()
407 defer s.mu.Unlock()
408 release, err := filelock.Acquire(ctx, s.path+".lock")
409 if err != nil {
410 return err
411 }
412 defer release()
413 state, err := load(s.path)
414 if err != nil {
415 return err
416 }
417 if commit == nil {
418 return nil
419 }
420 return commit(state)
421 }
422
423 func (s *Store) mutate(ctx context.Context, change func(*State) error) error {
424 if s == nil || strings.TrimSpace(s.path) == "" || s.path == "." {
425 return errors.New("workspace state path is required")
426 }
427 s.mu.Lock()
428 defer s.mu.Unlock()
429 if err := os.MkdirAll(filepath.Dir(s.path), 0o700); err != nil {
430 return err
431 }
432 release, err := filelock.Acquire(ctx, s.path+".lock")
433 if err != nil {
434 return err
435 }
436 defer release()
437 upgrading := false
438 if body, err := os.ReadFile(s.path); err == nil {
439 var header struct {
440 Version int `json:"version"`
441 }
442 upgrading = json.Unmarshal(body, &header) == nil && header.Version < SchemaVersion
443 }
444 if s.beforeUpgrade != nil {
445 body, readErr := os.ReadFile(s.path)
446 if readErr != nil && !os.IsNotExist(readErr) {
447 return readErr
448 }
449 var header struct {
450 Version int `json:"version"`
451 }
452 if len(body) > 0 && json.Unmarshal(body, &header) == nil && header.Version == 1 {
453 if err := s.beforeUpgrade(ctx); err != nil {
454 return err
455 }
456 }
457 }
458 if err := backupV1(s.path); err != nil {
459 return err
460 }
461 if err := backupV2(s.path); err != nil {
462 return err
463 }
464 state, err := load(s.path)
465 if err != nil {
466 return err
467 }
468 before, err := json.Marshal(state)
469 if err != nil {
470 return err
471 }
472 if err := change(&state); err != nil {
473 return err
474 }
475 after, err := json.Marshal(state)
476 if err != nil {
477 return err
478 }
479 if bytes.Equal(before, after) && !upgrading {
480 return nil
481 }
482 state.Generation++
483 state.Initialized = true
484 normalize(&state)
485 if err := validate(state); err != nil {
486 return err
487 }
488 body, err := json.Marshal(state)
489 if err != nil {
490 return err
491 }
492 return fileutil.AtomicWriteFileStrict(s.path, append(body, '\n'), 0o600)
493 }
494
495 func load(path string) (State, error) {
496 body, err := os.ReadFile(path)
497 if errors.Is(err, os.ErrNotExist) {
498 return newState(), nil
499 }
500 if err != nil {
501 return State{}, err
502 }
503 var state State
504 if err := json.Unmarshal(body, &state); err != nil {
505 return State{}, fmt.Errorf("decode workspace state: %w", err)
506 }
507 if state.Version != 1 && state.Version != 2 && state.Version != SchemaVersion {
508 return State{}, fmt.Errorf("%w: %d", ErrUnsupportedVersion, state.Version)
509 }
510 if state.Version == 1 {
511 state.SessionStates = map[string]SessionState{}
512 for _, id := range state.ArchivedSessionIDs {
513 state.SessionStates[id] = SessionState{Lifecycle: Archived, Generation: state.Generation}
514 }
515 state.Version = SchemaVersion
516 } else {
517 if state.WorkspaceIDs == nil || state.Workspaces == nil || state.SessionStates == nil {
518 return State{}, fmt.Errorf("%w: missing required registry fields", ErrUnsupportedVersion)
519 }
520 for _, workspace := range state.Workspaces {
521 for _, id := range workspace.SessionIDs {
522 if _, ok := state.SessionStates[id]; !ok {
523 return State{}, fmt.Errorf("%w: missing session lifecycle", ErrUnsupportedVersion)
524 }
525 }
526 }
527 }
528 state.Version = SchemaVersion
529 normalize(&state)
530 if err := validate(state); err != nil {
531 return State{}, err
532 }
533 return state, nil
534 }
535
536 func newState() State {
537 state := State{Version: SchemaVersion, WorkspaceIDs: []string{}, Workspaces: map[string]Workspace{}, ArchivedSessionIDs: []string{}, PendingCreates: map[string]PendingCreate{}}
538 normalize(&state)
539 return state
540 }
541
542 func normalize(state *State) {
543 if state.SessionStates == nil {
544 state.SessionStates = map[string]SessionState{}
545 }
546 if state.SourceMappings == nil {
547 state.SourceMappings = map[string]SourceMapping{}
548 }
549 if state.PendingOperations == nil {
550 state.PendingOperations = map[string]Operation{}
551 }
552 if state.RecoveryEntries == nil {
553 state.RecoveryEntries = map[string]RecoveryEntry{}
554 }
555 if state.Presentation == nil {
556 state.Presentation = map[string]Presentation{}
557 }
558 if state.WorkspaceIDs == nil {
559 state.WorkspaceIDs = []string{}
560 }
561 if state.Workspaces == nil {
562 state.Workspaces = map[string]Workspace{}
563 }
564 if state.ArchivedSessionIDs == nil {
565 state.ArchivedSessionIDs = []string{}
566 }
567 if state.PendingCreates == nil {
568 state.PendingCreates = map[string]PendingCreate{}
569 }
570 for id, workspace := range state.Workspaces {
571 if workspace.SessionIDs == nil {
572 workspace.SessionIDs = []string{}
573 }
574 state.Workspaces[id] = workspace
575 for _, sessionID := range workspace.SessionIDs {
576 if _, ok := state.SessionStates[sessionID]; !ok {
577 state.SessionStates[sessionID] = SessionState{Lifecycle: Active, Generation: state.Generation}
578 }
579 }
580 }
581 state.ArchivedSessionIDs = []string{}
582 for id, status := range state.SessionStates {
583 if status.Lifecycle == Archived {
584 state.ArchivedSessionIDs = append(state.ArchivedSessionIDs, id)
585 }
586 }
587 slices.Sort(state.ArchivedSessionIDs)
588 }
589
590 func validate(state State) error {
591 if err := validateLifecycleState(state); err != nil {
592 return err
593 }
594 seen := map[string]struct{}{}
595 for _, id := range state.WorkspaceIDs {
596 if _, duplicate := seen[id]; duplicate {
597 return fmt.Errorf("workspace state has duplicate workspace %q", id)
598 }
599 seen[id] = struct{}{}
600 workspace, ok := state.Workspaces[id]
601 if !ok || workspace.ID != id {
602 return fmt.Errorf("workspace state has invalid workspace %q", id)
603 }
604 }
605 owners := map[string]string{}
606 for id, workspace := range state.Workspaces {
607 for _, sessionID := range workspace.SessionIDs {
608 if owner, duplicate := owners[sessionID]; duplicate {
609 return fmt.Errorf("session %q belongs to both %q and %q", sessionID, owner, id)
610 }
611 owners[sessionID] = id
612 }
613 }
614 return nil
615 }
616
617 func sessionOwner(state State, sessionID string) (string, bool) {
618 for id, workspace := range state.Workspaces {
619 if contains(workspace.SessionIDs, sessionID) {
620 return id, true
621 }
622 }
623 return "", false
624 }
625
626 func insertBefore(ids []string, id, before string) []string {
627 ids = remove(ids, id)
628 if before != "" {
629 for i, current := range ids {
630 if current == before {
631 return append(append(append([]string{}, ids[:i]...), id), ids[i:]...)
632 }
633 }
634 }
635 return append(ids, id)
636 }
637
638 func remove(ids []string, target string) []string {
639 result := make([]string, 0, len(ids))
640 for _, id := range ids {
641 if id != target {
642 result = append(result, id)
643 }
644 }
645 return result
646 }
647
648 func contains(ids []string, target string) bool {
649 return slices.Contains(ids, target)
650 }
651
652 func cloneState(state State) (State, error) {
653 body, err := json.Marshal(state)
654 if err != nil {
655 return State{}, err
656 }
657 var clone State
658 if err := json.Unmarshal(body, &clone); err != nil {
659 return State{}, err
660 }
661 normalize(&clone)
662 return clone, nil
663 }
664
665 func (s *State) UnmarshalJSON(body []byte) error {
666 type plain State
667 var decoded plain
668 if err := json.Unmarshal(body, &decoded); err != nil {
669 return err
670 }
671 var fields map[string]json.RawMessage
672 if err := json.Unmarshal(body, &fields); err != nil {
673 return err
674 }
675 for _, key := range []string{"version", "generation", "initialized", "workspaceIds", "workspaces", "archivedSessionIds", "pendingCreates", "sessionStates", "sourceMappings", "pendingOperations", "recoveryEntries", "presentation"} {
676 delete(fields, key)
677 }
678 *s = State(decoded)
679 s.extra = fields
680 return nil
681 }
682
683 func (s State) MarshalJSON() ([]byte, error) {
684 type plain State
685 body, err := json.Marshal(plain(s))
686 if err != nil {
687 return nil, err
688 }
689 return mergeUnknown(body, s.extra)
690 }
691
692 func (w *Workspace) UnmarshalJSON(body []byte) error {
693 type plain Workspace
694 var decoded plain
695 if err := json.Unmarshal(body, &decoded); err != nil {
696 return err
697 }
698 var fields map[string]json.RawMessage
699 if err := json.Unmarshal(body, &fields); err != nil {
700 return err
701 }
702 for _, key := range []string{"id", "root", "title", "sessionIds", "visible", "createdAt", "updatedAt", "organization"} {
703 delete(fields, key)
704 }
705 *w = Workspace(decoded)
706 w.extra = fields
707 return nil
708 }
709
710 func (w Workspace) MarshalJSON() ([]byte, error) {
711 type plain Workspace
712 body, err := json.Marshal(plain(w))
713 if err != nil {
714 return nil, err
715 }
716 return mergeUnknown(body, w.extra)
717 }
718
719 func mergeUnknown(known []byte, extra map[string]json.RawMessage) ([]byte, error) {
720 if len(extra) == 0 {
721 return known, nil
722 }
723 var fields map[string]json.RawMessage
724 if err := json.Unmarshal(known, &fields); err != nil {
725 return nil, err
726 }
727 for key, value := range extra {
728 if _, exists := fields[key]; !exists {
729 fields[key] = bytes.Clone(value)
730 }
731 }
732 return json.Marshal(fields)
733 }
734
734 lines GO