Class: Mbeditor::EditorStateService

Inherits:
Object
  • Object
show all
Defined in:
app/services/mbeditor/editor_state_service.rb

Constant Summary collapse

PayloadTooLargeError =
Class.new(StandardError)
InvalidBranchError =
Class.new(StandardError)
LockTimeoutError =
Class.new(StandardError)
STATE_MAX_BYTES =
1 * 1024 * 1024
SAFE_BRANCH_NAME =
/\A[a-zA-Z0-9._\-\/]+\z/
DEFAULT_LOCK_TIMEOUT =

State writes take an exclusive file lock shared with EditorChannel. A blocking acquire would let a single stuck holder (e.g. a request paused at a breakpoint mid-write) wedge every later save indefinitely, so the lock is acquired non-blocking with a bounded retry and gives up with a clear error rather than hanging the worker.

5.0
LOCK_RETRY_INTERVAL =
0.01

Instance Method Summary collapse

Constructor Details

#initialize(workspace_root, lock_timeout: DEFAULT_LOCK_TIMEOUT) ⇒ EditorStateService

Returns a new instance of EditorStateService.



19
20
21
22
# File 'app/services/mbeditor/editor_state_service.rb', line 19

def initialize(workspace_root, lock_timeout: DEFAULT_LOCK_TIMEOUT)
  @root = workspace_root
  @lock_timeout = lock_timeout
end

Instance Method Details

#prune_branch_states(active_branches:) ⇒ Object



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'app/services/mbeditor/editor_state_service.rb', line 62

def prune_branch_states(active_branches:)
  path = branch_states_path
  return [] unless File.exist?(path)
  pruned = []
  File.open(path, File::RDWR) do |f|
    lock_exclusive!(f)
    all = begin
      JSON.parse(f.read)
    rescue JSON::ParserError => e
      Rails.logger.error("[mbeditor] EditorStateService#prune_branch_states: discarding corrupt branch_states JSON at #{path}: #{e.message}")
      {}
    end
    pruned = all.keys - active_branches
    if pruned.any?
      pruned.each { |b| all.delete(b) }
      f.truncate(0)
      f.rewind
      f.write(all.to_json)
    end
  end
  pruned
end

#read_branch_state(branch) ⇒ Object



32
33
34
35
36
37
38
39
# File 'app/services/mbeditor/editor_state_service.rb', line 32

def read_branch_state(branch)
  path = branch_states_path
  return {} unless File.exist?(path)
  all = JSON.parse(File.read(path))
  all[branch] || {}
rescue JSON::ParserError, Errno::ENOENT
  {}
end

#read_stateObject



24
25
26
27
28
29
30
# File 'app/services/mbeditor/editor_state_service.rb', line 24

def read_state
  path = workspace_path
  return {} unless File.exist?(path)
  JSON.parse(File.read(path))
rescue JSON::ParserError, Errno::ENOENT
  {}
end

#write_branch_state(branch, state) ⇒ Object

Raises:



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'app/services/mbeditor/editor_state_service.rb', line 41

def write_branch_state(branch, state)
  raise InvalidBranchError, "Invalid branch name" unless branch.match?(SAFE_BRANCH_NAME)
  payload_json = state.to_json
  raise PayloadTooLargeError, "State payload too large" if payload_json.bytesize > STATE_MAX_BYTES
  path = branch_states_path
  FileUtils.mkdir_p(path.dirname)
  File.open(path, File::RDWR | File::CREAT) do |f|
    lock_exclusive!(f)
    existing = f.size > 0 ? JSON.parse(f.read) : {}
    # Auto-save fires on a timer even with no changes; skip the full-file
    # rewrite when this branch's entry is already identical.
    break if existing[branch] == JSON.parse(payload_json)

    existing[branch] = state
    f.truncate(0)
    f.rewind
    f.write(existing.to_json)
  end
  nil
end

#write_state(state) ⇒ Object



85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'app/services/mbeditor/editor_state_service.rb', line 85

def write_state(state)
  payload = state.to_json
  raise PayloadTooLargeError, "State payload too large" if payload.bytesize > STATE_MAX_BYTES
  path = workspace_path
  FileUtils.mkdir_p(path.dirname)
  File.open(path, File::RDWR | File::CREAT) do |f|
    lock_exclusive!(f)
    f.truncate(0)
    f.rewind
    f.write(payload)
  end
  nil
end