Class: RailsMarkup::Store
- Inherits:
-
Object
- Object
- RailsMarkup::Store
- 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, CapacityError, Session, ValidationError
Constant Summary collapse
- MAX_SESSIONS =
100- SESSION_TTL =
4 hours
4 * 3600
- MAX_ANNOTATIONS_PER_SESSION =
1_000- MAX_ANNOTATION_BYTES =
25_000_000- MAX_CONTENT_BYTES =
5_000- MAX_TARGET_BYTES =
16_384- MAX_SELECTED_TEXT_BYTES =
2_000- MAX_METADATA_BYTES =
65_536- MAX_THREAD_MESSAGE_BYTES =
5_000- INTENTS =
%w[fix change question approve].freeze
- SEVERITIES =
%w[suggestion important blocking].freeze
Instance Attribute Summary collapse
-
#sessions ⇒ Object
readonly
Returns the value of attribute sessions.
Instance Method Summary collapse
-
#acknowledge(annotation_id) ⇒ Object
--- Status transitions ---.
- #all_pending ⇒ Object
-
#create_annotation(session_id:, target:, content:, intent: "change", severity: "suggestion", selected_text: nil, metadata: {}) ⇒ Object
--- Annotations ---.
-
#create_session(url:, metadata: {}) ⇒ Object
--- Sessions ---.
- #dismiss(annotation_id, reason: nil) ⇒ Object
- #get_annotation(annotation_id) ⇒ Object
- #get_session(id) ⇒ Object
-
#initialize(max_annotations_per_session: MAX_ANNOTATIONS_PER_SESSION, max_annotation_bytes: MAX_ANNOTATION_BYTES) ⇒ Store
constructor
A new instance of Store.
- #list_sessions ⇒ Object
- #pending_for_session(session_id) ⇒ Object
- #reply(annotation_id, message:) ⇒ Object
- #resolve(annotation_id, summary: nil) ⇒ Object
- #serialize_annotation(ann) ⇒ Object
-
#serialize_session(session) ⇒ Object
--- Serialization ---.
-
#subscribe(session_id, &callback) ⇒ Object
--- SSE subscriptions ---.
- #supports_subscriptions? ⇒ Boolean
- #unsubscribe(sub) ⇒ Object
Constructor Details
#initialize(max_annotations_per_session: MAX_ANNOTATIONS_PER_SESSION, max_annotation_bytes: MAX_ANNOTATION_BYTES) ⇒ Store
Returns a new instance of Store.
31 32 33 34 35 36 37 38 39 |
# File 'lib/rails_markup/store.rb', line 31 def initialize(max_annotations_per_session: MAX_ANNOTATIONS_PER_SESSION, max_annotation_bytes: MAX_ANNOTATION_BYTES) @sessions = {} @annotations_index = {} # id -> annotation (O(1) lookup) @subscribers = [] # SSE callbacks: [session_id, callback] @mutex = Mutex.new @max_annotations_per_session = max_annotations_per_session @max_annotation_bytes = max_annotation_bytes end |
Instance Attribute Details
#sessions ⇒ Object (readonly)
Returns the value of attribute sessions.
29 30 31 |
# File 'lib/rails_markup/store.rb', line 29 def sessions @sessions end |
Instance Method Details
#acknowledge(annotation_id) ⇒ Object
--- Status transitions ---
122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/rails_markup/store.rb', line 122 def acknowledge(annotation_id) @mutex.synchronize do ann = @annotations_index[annotation_id] return nil unless ann return ann if ann.status == "acknowledged" validate_transition!(ann, "acknowledged", from: %w[pending]) ann.status = "acknowledged" ann end end |
#all_pending ⇒ Object
114 115 116 117 118 |
# File 'lib/rails_markup/store.rb', line 114 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 ---
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 96 97 98 99 100 101 |
# File 'lib/rails_markup/store.rb', line 69 def create_annotation(session_id:, target:, content:, intent: "change", severity: "suggestion", selected_text: nil, metadata: {}) annotation_bytes = validate_annotation!( target:, content:, intent:, severity:, selected_text:, 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 enforce_session_capacity!(session, annotation_bytes) 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 ---
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/rails_markup/store.rb', line 43 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
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/rails_markup/store.rb', line 154 def dismiss(annotation_id, reason: nil) changed = false ann = @mutex.synchronize do annotation = @annotations_index[annotation_id] return nil unless annotation return annotation if annotation.status == "dismissed" validate_transition!(annotation, "dismissed", from: %w[pending acknowledged]) (annotation, reason) unless reason.nil? || reason == "" annotation.status = "dismissed" changed = true annotation end return ann unless changed notify(ann.session_id, type: "annotation_update", annotation: serialize_annotation(ann), status: "dismissed", reason: reason) ann end |
#get_annotation(annotation_id) ⇒ Object
103 104 105 |
# File 'lib/rails_markup/store.rb', line 103 def get_annotation(annotation_id) @mutex.synchronize { @annotations_index[annotation_id] } end |
#get_session(id) ⇒ Object
59 60 61 |
# File 'lib/rails_markup/store.rb', line 59 def get_session(id) @mutex.synchronize { @sessions[id] } end |
#list_sessions ⇒ Object
63 64 65 |
# File 'lib/rails_markup/store.rb', line 63 def list_sessions @mutex.synchronize { @sessions.values } end |
#pending_for_session(session_id) ⇒ Object
107 108 109 110 111 112 |
# File 'lib/rails_markup/store.rb', line 107 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
174 175 176 177 178 179 180 181 182 183 184 185 |
# File 'lib/rails_markup/store.rb', line 174 def reply(annotation_id, message:) ann = @mutex.synchronize do annotation = @annotations_index[annotation_id] return nil unless annotation (annotation, ) annotation end notify(ann.session_id, type: "annotation_update", annotation: serialize_annotation(ann), status: ann.status, message: ) ann end |
#resolve(annotation_id, summary: nil) ⇒ Object
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/rails_markup/store.rb', line 134 def resolve(annotation_id, summary: nil) changed = false ann = @mutex.synchronize do annotation = @annotations_index[annotation_id] return nil unless annotation return annotation if annotation.status == "resolved" validate_transition!(annotation, "resolved", from: %w[pending acknowledged]) (annotation, summary) unless summary.nil? || summary == "" annotation.status = "resolved" changed = true annotation end return ann unless changed notify(ann.session_id, type: "annotation_update", annotation: serialize_annotation(ann), status: "resolved", summary: summary) ann end |
#serialize_annotation(ann) ⇒ Object
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 |
# File 'lib/rails_markup/store.rb', line 215 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 ---
205 206 207 208 209 210 211 212 213 |
# File 'lib/rails_markup/store.rb', line 205 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 ---
189 190 191 192 193 |
# File 'lib/rails_markup/store.rb', line 189 def subscribe(session_id, &callback) sub = [session_id, callback] @mutex.synchronize { @subscribers << sub } sub end |
#supports_subscriptions? ⇒ Boolean
199 200 201 |
# File 'lib/rails_markup/store.rb', line 199 def supports_subscriptions? true end |
#unsubscribe(sub) ⇒ Object
195 196 197 |
# File 'lib/rails_markup/store.rb', line 195 def unsubscribe(sub) @mutex.synchronize { @subscribers.delete(sub) } end |