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



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'app/services/mbeditor/editor_state_service.rb', line 54

def prune_branch_states(active_branches:)
  path = branch_states_path
  return [] unless File.exist?(path)
  pruned = []
  with_lock(path) do
    all = begin
      read_json(path)
    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) }
      atomic_write(path, all.to_json)
    end
  end
  pruned
end

#read_branch_state(branch) ⇒ Object



30
31
32
33
34
# File 'app/services/mbeditor/editor_state_service.rb', line 30

def read_branch_state(branch)
  read_json(branch_states_path)[branch] || {}
rescue JSON::ParserError, Errno::ENOENT
  {}
end

#read_stateObject



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

def read_state
  read_json(workspace_path)
rescue JSON::ParserError, Errno::ENOENT
  {}
end

#write_branch_state(branch, state) ⇒ Object

Raises:



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'app/services/mbeditor/editor_state_service.rb', line 36

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)
  with_lock(path) do
    existing = read_json(path)
    # Auto-save fires on a timer even with no changes; skip the full-file
    # rewrite when this branch's entry is already identical.
    next if existing[branch] == JSON.parse(payload_json)

    existing[branch] = state
    atomic_write(path, existing.to_json)
  end
  nil
end

#write_state(state) ⇒ Object



74
75
76
77
78
79
80
81
# File 'app/services/mbeditor/editor_state_service.rb', line 74

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)
  with_lock(path) { atomic_write(path, payload) }
  nil
end