Class: Msnav::WindowRegistry

Inherits:
Object
  • Object
show all
Defined in:
lib/msnav/windows.rb

Overview

DB-backed registry of live editor windows for bridge-mode open routing — the Ruby side of coderag/daemon/windows.py, byte-compatible on the shared windows / window_commands tables so msnav daemons in Ruby containers and coderag daemons elsewhere route opens to each other's windows.

Each window registers and then short-polls for commands. When /api/open targets a file another live window owns, the command is enqueued in the DB (the message bus between containers) and that window's poll delivers it. A window counts as live only while it keeps polling.

Defined Under Namespace

Classes: WindowReg

Constant Summary collapse

ALIVE_SECONDS =

a registration is live only if it polled this recently

6.0
PURGE_SECONDS =

rows this stale are garbage-collected on registration

3600.0
PARK_SECONDS =

A parked open-command (no owning window yet — a new window is being opened for it) waits this long for that window's extension to register. Generous: a first-time "Reopen in Container" builds the image first.

600.0
PARKED =

window_id marker for parked commands.

""
COLS =
"window_id, authority, roots, path_mappings, host_roots, seq, last_poll"

Instance Method Summary collapse

Constructor Details

#initialize(store) ⇒ WindowRegistry

Returns a new instance of WindowRegistry.



34
35
36
# File 'lib/msnav/windows.rb', line 34

def initialize(store)
  @store = store
end

Instance Method Details

#allObject



184
185
186
187
188
189
# File 'lib/msnav/windows.rb', line 184

def all
  rows = @store.with_conn do |db|
    db.execute("SELECT #{COLS} FROM windows ORDER BY seq")
  end
  rows.map { |r| row_to_reg(r) }
end

#countObject



38
39
40
# File 'lib/msnav/windows.rb', line 38

def count
  @store.with_conn { |db| db.get_first_value("SELECT COUNT(*) FROM windows") }
end

#drain(window_id) ⇒ Object

Return and clear the window's pending commands — including ones enqueued by a different daemon sharing this DB.



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/msnav/windows.rb', line 111

def drain(window_id)
  rows = nil
  @store.with_conn do |db|
    db.transaction(:immediate) do
      rows = db.execute(
        "SELECT id, payload FROM window_commands " \
        "WHERE window_id = ? ORDER BY id", [window_id])
      unless rows.empty?
        db.execute(
          "DELETE FROM window_commands WHERE id <= ? AND window_id = ?",
          [rows[-1][0], window_id])
      end
      db.execute("UPDATE windows SET last_poll = ? WHERE window_id = ?",
                 [Time.now.to_f, window_id])
    end
  end
  rows.map { |_id, payload| JSON.parse(payload) }
end

#enqueue_open(host_path, line) ⇒ Object

Queue an open-command on the live window that owns HOST_PATH; returns the matched registration, or nil when no live window claims it.



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/msnav/windows.rb', line 132

def enqueue_open(host_path, line)
  now = Time.now.to_f
  reg = nil
  @store.with_conn do |db|
    db.transaction(:immediate) do
      rows = db.execute(
        "SELECT #{COLS} FROM windows WHERE last_poll >= ? " \
        "ORDER BY seq DESC", [now - ALIVE_SECONDS])
      rows.each do |row|
        candidate = row_to_reg(row)
        if candidate.host_roots.any? { |hr| under?(hr, host_path) }
          reg = candidate
          break
        end
      end
      unless reg.nil?
        db.execute(
          "INSERT INTO window_commands (window_id, payload, created_at) " \
          "VALUES (?, ?, ?)",
          [reg.window_id,
           JSON.generate("type" => "open", "path" => host_path,
                         "line" => line), now])
      end
    end
  end
  reg
end

#park_open(host_path, line) ⇒ Object

Queue an open for a window that does not exist yet: the caller is opening a new editor window for this service, and its extension will claim the command when it registers.



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/msnav/windows.rb', line 163

def park_open(host_path, line)
  now = Time.now.to_f
  @store.with_conn do |db|
    db.transaction(:immediate) do
      # re-clicking must not stack duplicate opens for the same file
      db.execute(
        "SELECT id, payload FROM window_commands WHERE window_id = ?",
        [PARKED]).each do |cmd_id, payload|
        next unless JSON.parse(payload)["path"] == host_path
        db.execute("DELETE FROM window_commands WHERE id = ?", [cmd_id])
      end
      db.execute(
        "INSERT INTO window_commands (window_id, payload, created_at) " \
        "VALUES (?, ?, ?)",
        [PARKED, JSON.generate("type" => "open", "path" => host_path,
                               "line" => line), now])
    end
  end
  nil
end

#register(window_id, authority, roots, path_mappings) ⇒ Object

Record (or refresh) a window. A reload re-registers with a fresh window_id but identical roots — stale entries with the same mapped (daemon-space) roots are dropped so only the live window remains.



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/msnav/windows.rb', line 45

def register(window_id, authority, roots, path_mappings)
  roots = Array(roots)
  pmaps = Array(path_mappings)
  host_roots = roots.map { |r| root_to_host(pmaps, r) }.uniq.sort
  hr_json = JSON.generate(host_roots)
  now = Time.now.to_f
  reg = nil
  @store.with_conn do |db|
    db.transaction(:immediate) do
      db.execute(
        "DELETE FROM window_commands WHERE window_id IN " \
        "(SELECT window_id FROM windows WHERE last_poll < ?)",
        [now - PURGE_SECONDS])
      db.execute("DELETE FROM windows WHERE last_poll < ?",
                 [now - PURGE_SECONDS])
      # expired parked commands: the window they waited for never came
      db.execute(
        "DELETE FROM window_commands WHERE window_id = ? AND created_at < ?",
        [PARKED, now - PARK_SECONDS])
      db.execute(
        "DELETE FROM windows WHERE host_roots = ? AND window_id != ?",
        [hr_json, window_id])
      seq = db.get_first_value(
        "SELECT COALESCE(MAX(seq), 0) + 1 FROM windows")
      db.execute(
        "INSERT INTO windows (window_id, authority, roots, " \
        "path_mappings, host_roots, seq, last_poll) " \
        "VALUES (?, ?, ?, ?, ?, ?, ?) " \
        "ON CONFLICT(window_id) DO UPDATE SET authority = excluded.authority, " \
        "roots = excluded.roots, path_mappings = excluded.path_mappings, " \
        "host_roots = excluded.host_roots, seq = excluded.seq, " \
        "last_poll = excluded.last_poll",
        [window_id, authority || "", JSON.generate(roots),
         JSON.generate(pmaps), hr_json, seq, now])
      # hand parked commands to the window that was opened FOR them: the
      # first registration whose roots cover a parked path claims it
      db.execute(
        "SELECT id, payload FROM window_commands " \
        "WHERE window_id = ? ORDER BY id", [PARKED]).each do |cmd_id, payload|
        target = JSON.parse(payload)["path"] || ""
        next unless host_roots.any? { |hr| under?(hr, target) }
        db.execute("UPDATE window_commands SET window_id = ? WHERE id = ?",
                   [window_id, cmd_id])
      end
      reg = WindowReg.new(window_id: window_id, authority: authority || "",
                          roots: roots, path_mappings: pmaps,
                          host_roots: host_roots, seq: seq, last_poll: now)
    end
  end
  reg
end

#touch(window_id) ⇒ Object

Mark a window as still-alive on poll; nil if it isn't registered.



98
99
100
101
102
103
104
105
106
107
# File 'lib/msnav/windows.rb', line 98

def touch(window_id)
  @store.with_conn do |db|
    db.execute("UPDATE windows SET last_poll = ? WHERE window_id = ?",
               [Time.now.to_f, window_id])
    return nil if db.changes.zero?
    row = db.get_first_row(
      "SELECT #{COLS} FROM windows WHERE window_id = ?", [window_id])
    row && row_to_reg(row)
  end
end