Class: RailsMarkup::Store

Inherits:
Object
  • Object
show all
Defined in:
lib/rails_markup/store.rb

Overview

In-memory store for sessions and annotations. Ephemeral by design — data lives for one coding session.

Defined Under Namespace

Classes: Annotation, Session

Constant Summary collapse

MAX_SESSIONS =
100
SESSION_TTL =

4 hours

4 * 3600

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeStore

Returns a new instance of Store.



19
20
21
22
23
24
# File 'lib/rails_markup/store.rb', line 19

def initialize
  @sessions          = {}
  @annotations_index = {} # id -> annotation (O(1) lookup)
  @subscribers       = [] # SSE callbacks: [session_id, callback]
  @mutex             = Mutex.new
end

Instance Attribute Details

#sessionsObject (readonly)

Returns the value of attribute sessions.



17
18
19
# File 'lib/rails_markup/store.rb', line 17

def sessions
  @sessions
end

Instance Method Details

#acknowledge(annotation_id) ⇒ Object

--- Status transitions ---



103
104
105
# File 'lib/rails_markup/store.rb', line 103

def acknowledge(annotation_id)
  update_status(annotation_id, "acknowledged")
end

#all_pendingObject



95
96
97
98
99
# File 'lib/rails_markup/store.rb', line 95

def all_pending
  @mutex.synchronize do
    @sessions.values.flat_map { |s| s.annotations.select { |a| a.status == "pending" } }
  end
end

#create_annotation(session_id:, target:, content:, intent: "change", severity: "suggestion", selected_text: nil, metadata: {}) ⇒ Object

--- Annotations ---



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
# File 'lib/rails_markup/store.rb', line 54

def create_annotation(session_id:, target:, content:, intent: "change", severity: "suggestion",
                      selected_text: nil, metadata: {})
  id = SecureRandom.hex(8)
  annotation = Annotation.new(
    id: id,
    session_id: session_id,
    target: target,
    content: content,
    intent: intent,
    severity: severity,
    status: "pending",
    selected_text: selected_text,
    metadata:  || {},
    created_at: Time.now.iso8601,
    thread: []
  )

  # Single mutex block — no TOCTOU gap
  @mutex.synchronize do
    session = @sessions[session_id]
    return nil unless session

    session.annotations << annotation
    @annotations_index[id] = annotation
  end

  notify(session_id, type: "annotation_created", annotation: serialize_annotation(annotation))
  annotation
end

#create_session(url:, metadata: {}) ⇒ Object

--- Sessions ---



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/rails_markup/store.rb', line 28

def create_session(url:, metadata: {})
  id = SecureRandom.hex(8)
  session = Session.new(
    id: id,
    url: url,
    metadata:  || {},
    created_at: Time.now.iso8601,
    annotations: []
  )
  @mutex.synchronize do
    evict_stale_sessions
    @sessions[id] = session
  end
  session
end

#dismiss(annotation_id, reason: nil) ⇒ Object



117
118
119
120
121
122
123
124
125
# File 'lib/rails_markup/store.rb', line 117

def dismiss(annotation_id, reason: nil)
  ann = update_status(annotation_id, "dismissed")
  return nil unless ann

  ann.thread << { role: "agent", message: reason, timestamp: Time.now.iso8601 } if reason
  notify(ann.session_id, type: "annotation_update", annotation: serialize_annotation(ann),
                         status: "dismissed", reason: reason)
  ann
end

#get_annotation(annotation_id) ⇒ Object



84
85
86
# File 'lib/rails_markup/store.rb', line 84

def get_annotation(annotation_id)
  @mutex.synchronize { @annotations_index[annotation_id] }
end

#get_session(id) ⇒ Object



44
45
46
# File 'lib/rails_markup/store.rb', line 44

def get_session(id)
  @mutex.synchronize { @sessions[id] }
end

#list_sessionsObject



48
49
50
# File 'lib/rails_markup/store.rb', line 48

def list_sessions
  @mutex.synchronize { @sessions.values }
end

#pending_for_session(session_id) ⇒ Object



88
89
90
91
92
93
# File 'lib/rails_markup/store.rb', line 88

def pending_for_session(session_id)
  session = get_session(session_id)
  return [] unless session

  @mutex.synchronize { session.annotations.select { |a| a.status == "pending" } }
end

#reply(annotation_id, message:) ⇒ Object



127
128
129
130
131
132
133
134
135
136
137
# File 'lib/rails_markup/store.rb', line 127

def reply(annotation_id, message:)
  ann = get_annotation(annotation_id)
  return nil unless ann

  @mutex.synchronize do
    ann.thread << { role: "agent", message: message, timestamp: Time.now.iso8601 }
  end
  notify(ann.session_id, type: "annotation_update", annotation: serialize_annotation(ann),
                         status: ann.status, message: message)
  ann
end

#resolve(annotation_id, summary: nil) ⇒ Object



107
108
109
110
111
112
113
114
115
# File 'lib/rails_markup/store.rb', line 107

def resolve(annotation_id, summary: nil)
  ann = update_status(annotation_id, "resolved")
  return nil unless ann

  ann.thread << { role: "agent", message: summary, timestamp: Time.now.iso8601 } if summary
  notify(ann.session_id, type: "annotation_update", annotation: serialize_annotation(ann),
                         status: "resolved", summary: summary)
  ann
end

#serialize_annotation(ann) ⇒ Object



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/rails_markup/store.rb', line 163

def serialize_annotation(ann)
  {
    id: ann.id,
    sessionId: ann.session_id,
    target: ann.target,
    content: ann.content,
    intent: ann.intent,
    severity: ann.severity,
    status: ann.status,
    selectedText: ann.selected_text,
    authorName: ann.&.dig("author"),
    metadata: ann.,
    createdAt: ann.created_at,
    thread: ann.thread
  }
end

#serialize_session(session) ⇒ Object

--- Serialization ---



153
154
155
156
157
158
159
160
161
# File 'lib/rails_markup/store.rb', line 153

def serialize_session(session)
  {
    id: session.id,
    url: session.url,
    metadata: session.,
    createdAt: session.created_at,
    annotations: session.annotations.map { |a| serialize_annotation(a) }
  }
end

#subscribe(session_id, &callback) ⇒ Object

--- SSE subscriptions ---



141
142
143
144
145
# File 'lib/rails_markup/store.rb', line 141

def subscribe(session_id, &callback)
  sub = [session_id, callback]
  @mutex.synchronize { @subscribers << sub }
  sub
end

#unsubscribe(sub) ⇒ Object



147
148
149
# File 'lib/rails_markup/store.rb', line 147

def unsubscribe(sub)
  @mutex.synchronize { @subscribers.delete(sub) }
end