rails-markup 1.4.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c2efa8f1fca5e20c13c63a7157b47553cb893df6384bb0799b57b1dd9caa2bb6
4
- data.tar.gz: 5b244141b9d9bab054f7baa2f15dd3af9560e0ee3aaefcaeaef81c5e4e3541bd
3
+ metadata.gz: 665bd02a5e8830d8565296c7623f4136513a61a2c1574e68c0c2b6802daf7ea2
4
+ data.tar.gz: fd6290208722a52ecc255f3d01cdf017de31010157f6c67cc4386423e848eb98
5
5
  SHA512:
6
- metadata.gz: d6b1da9212c6140e1db0767b6a27b972497063657a8b5f23d28e1ba84d21d1ed606473182516730708bd7f29447df448baf967f34cf291de40374b724b984cd4
7
- data.tar.gz: 73f188f0de27d3fc628bc31388daa1a6e0048a124c3d1a0dfb91dcf9be0b048470179c4b47be58b403c2850653064d005fde99f10738bb394433c4cd17ba0cbd
6
+ metadata.gz: 32afe3f8d8ea940731cde32f9524738189ca7d54b1bc1d124adc8888adb9fce35347647bc5844ca15dac6c918a546cef2d9ca048e0901e98ebc87f17db34ec4e
7
+ data.tar.gz: 2c173c2623816601f0cc5ab730960cc035f2523a14adad0de4d130163f358d47562959f8fbd6c4b9e2e2ea9a0442e6d711dc6ba35070d89ae7d23e17c6cb2b15
@@ -43,6 +43,7 @@
43
43
  _syncMaxRetryDelay: 30000,
44
44
  _syncMalformedLimit: 3,
45
45
  _syncUnavailable: null,
46
+ legacyStorageEndpoint: null,
46
47
 
47
48
  // Drawing state
48
49
  drawingMode: null, // null | "arrow" | "rect" | "highlight"
@@ -73,6 +74,7 @@
73
74
  this.size = opts.size || "default";
74
75
  this.fabVisible = opts.fabVisible !== false;
75
76
  this.enableScreenshots = opts.enableScreenshots !== false;
77
+ this.legacyStorageEndpoint = opts.legacyStorageEndpoint || this.legacyStorageEndpoint;
76
78
  this.healthIntervalMs = (opts.healthInterval || 60) * 1000;
77
79
 
78
80
  if (document.getElementById("rm-toolbar-root")) {
@@ -1215,13 +1217,14 @@
1215
1217
  continue;
1216
1218
  }
1217
1219
  if (result.kind === "conflict") {
1218
- const pulled = await this._pullAnnotations();
1219
- if (pulled && this.outbox[clientId]) {
1220
+ const resolution = this._reconcileSyncConflict(snapshot, result);
1221
+ await this._pullAnnotations();
1222
+ if (resolution === "retry" && this.outbox[clientId]) {
1220
1223
  clientIds.push(clientId);
1221
1224
  continue;
1222
1225
  }
1223
- this._scheduleSyncRetry();
1224
- break;
1226
+ this._resetSyncRetry();
1227
+ continue;
1225
1228
  }
1226
1229
  if (result.kind === "malformed") {
1227
1230
  if (!this._outboxEntryMatches(snapshot)) {
@@ -1250,7 +1253,11 @@
1250
1253
  redirect: "manual",
1251
1254
  signal: AbortSignal.timeout(5000)
1252
1255
  };
1253
- if (snapshot.type === "upsert") {
1256
+ if (snapshot.type === "delete") {
1257
+ options.body = JSON.stringify({
1258
+ baseRevision: Number.isInteger(snapshot.baseRevision) ? snapshot.baseRevision : 0
1259
+ });
1260
+ } else {
1254
1261
  options.body = JSON.stringify(Object.assign({}, snapshot.annotation, {
1255
1262
  dirtyFields: snapshot.dirtyFields || [],
1256
1263
  baseRevision: Number.isInteger(snapshot.baseRevision) ? snapshot.baseRevision : 0
@@ -1292,7 +1299,7 @@
1292
1299
  if ([408, 425, 429].includes(status) || status >= 500) {
1293
1300
  return { kind: "retryable", retryAfter: this._retryAfterDelay(response) };
1294
1301
  }
1295
- if (status === 409 && snapshot.type === "upsert") return { kind: "conflict" };
1302
+ if (status === 409) return this._classifyConflictResponse(snapshot, response);
1296
1303
  if (status >= 400) return { kind: "terminal" };
1297
1304
  if (!response.ok) return { kind: "retryable" };
1298
1305
  if (snapshot.type === "delete") return { kind: "success", data: null };
@@ -1311,6 +1318,25 @@
1311
1318
  }
1312
1319
  },
1313
1320
 
1321
+ async _classifyConflictResponse(snapshot, response) {
1322
+ const contentType = response.headers.get("Content-Type") || "";
1323
+ if (!contentType.toLowerCase().includes("application/json")) return { kind: "malformed" };
1324
+
1325
+ try {
1326
+ const body = await response.json();
1327
+ if (!this._plainObject(body) || !Object.prototype.hasOwnProperty.call(body, "annotation")) {
1328
+ return { kind: "malformed" };
1329
+ }
1330
+ if (body.annotation === null && snapshot.type === "upsert") {
1331
+ return { kind: "conflict", missing: true, data: null };
1332
+ }
1333
+ if (!this._validServerAnnotation(body.annotation, snapshot.clientId)) return { kind: "malformed" };
1334
+ return { kind: "conflict", missing: false, data: body.annotation };
1335
+ } catch {
1336
+ return { kind: "malformed" };
1337
+ }
1338
+ },
1339
+
1314
1340
  _validServerAnnotation(data, expectedClientId) {
1315
1341
  if (!this._plainObject(data)) return false;
1316
1342
  const required = [
@@ -1385,6 +1411,57 @@
1385
1411
  annotation.syncState = "synced";
1386
1412
  },
1387
1413
 
1414
+ _reconcileSyncConflict(snapshot, conflict) {
1415
+ if (!this._outboxEntryMatches(snapshot)) return "stop";
1416
+
1417
+ let resolution = "stop";
1418
+ const committed = this._commitLocalStateChange(() => {
1419
+ if (!this._outboxEntryMatches(snapshot)) return;
1420
+ const entry = this.outbox[snapshot.clientId];
1421
+ const annotation = this.annotations.find(candidate => candidate.clientId === snapshot.clientId);
1422
+
1423
+ if (snapshot.type === "delete") {
1424
+ delete this.outbox[snapshot.clientId];
1425
+ if (annotation) this._mergePulledAnnotation(annotation, conflict.data, null);
1426
+ else this.annotations.push(this._annotationFromServer(conflict.data));
1427
+ this._assignDisplayIds();
1428
+ return;
1429
+ }
1430
+
1431
+ if (conflict.missing) {
1432
+ if (entry.missingConflictRebased) {
1433
+ entry.syncState = "failed";
1434
+ if (annotation) annotation.syncState = "failed";
1435
+ return;
1436
+ }
1437
+ entry.baseRevision = 0;
1438
+ entry.missingConflictRebased = true;
1439
+ if (annotation) {
1440
+ annotation.serverId = null;
1441
+ annotation.serverRevision = 0;
1442
+ }
1443
+ resolution = "retry";
1444
+ return;
1445
+ }
1446
+
1447
+ if (!annotation) {
1448
+ entry.syncState = "failed";
1449
+ return;
1450
+ }
1451
+ this._mergePulledAnnotation(annotation, conflict.data, entry);
1452
+ entry.annotation = this._desiredState(annotation);
1453
+ entry.dirtyFields = (annotation.dirtyFields || []).slice();
1454
+ entry.baseRevision = conflict.data.revision;
1455
+ delete entry.missingConflictRebased;
1456
+ resolution = "retry";
1457
+ });
1458
+ if (!committed) return "stop";
1459
+ this._renderPins();
1460
+ this._rebuildList();
1461
+ this._updateCount();
1462
+ return resolution;
1463
+ },
1464
+
1388
1465
  _markSyncFailed(snapshot) {
1389
1466
  if (!this._outboxEntryMatches(snapshot)) return;
1390
1467
  this._commitLocalStateChange(() => {
@@ -1479,9 +1556,9 @@
1479
1556
  ? data.legacyMigrations
1480
1557
  : {};
1481
1558
  }
1482
- // Pre-1.3 storage had no endpoint identity. The first configured endpoint
1483
- // to load on this origin claims it once; successful consolidation removes
1484
- // the source keys so another endpoint cannot import the same data later.
1559
+ // Pre-1.3 bare storage has no endpoint provenance, so it is left intact
1560
+ // unless the host explicitly designates this endpoint. Page-qualified
1561
+ // legacy keys are only claimed by a toolbar currently on that exact page.
1485
1562
  const migratedKeys = this._migrateUnnamespacedStorage();
1486
1563
  migratedKeys.push(...this._migratePageAnnotations());
1487
1564
  this._normalizeStoredState();
@@ -1494,9 +1571,13 @@
1494
1571
 
1495
1572
  _migrateUnnamespacedStorage() {
1496
1573
  const sourceKeys = [];
1574
+ const designatedEndpoint = (this.legacyStorageEndpoint || "").replace(/\/+$/, "");
1575
+ const currentEndpoint = (this.endpoint || "").replace(/\/+$/, "");
1576
+ const claimBareStorage = Boolean(designatedEndpoint) && designatedEndpoint === currentEndpoint;
1577
+ const currentPageKey = `rm-annotations:${this._pageUrl()}`;
1497
1578
  for (let index = 0; index < localStorage.length; index++) {
1498
1579
  const key = localStorage.key(index);
1499
- if (key === "rm-annotations" || (key && key.startsWith("rm-annotations:/"))) sourceKeys.push(key);
1580
+ if ((claimBareStorage && key === "rm-annotations") || key === currentPageKey) sourceKeys.push(key);
1500
1581
  }
1501
1582
 
1502
1583
  const migratedKeys = [];
@@ -88,8 +88,21 @@ module RailsMarkup
88
88
  def destroy
89
89
  client_uuid = normalized_route_uuid
90
90
  return render_invalid_uuid unless client_uuid
91
+ base_revision = normalized_base_revision
92
+ return render_invalid_base_revision unless base_revision
93
+
94
+ annotation = Annotation.find_by(client_uuid: client_uuid)
95
+ return head :no_content unless annotation
96
+
97
+ annotation.with_lock do
98
+ raise Annotation::RevisionConflict, annotation unless annotation.revision == base_revision
91
99
 
92
- Annotation.find_by(client_uuid: client_uuid)&.destroy!
100
+ annotation.destroy!
101
+ end
102
+ head :no_content
103
+ rescue Annotation::RevisionConflict => error
104
+ render_revision_conflict(error)
105
+ rescue ActiveRecord::RecordNotFound
93
106
  head :no_content
94
107
  end
95
108
 
@@ -260,7 +273,7 @@ module RailsMarkup
260
273
  def render_revision_conflict(error)
261
274
  render json: {
262
275
  error: "revision conflict",
263
- annotation: error.annotation.as_api_json
276
+ annotation: error.annotation.new_record? ? nil : error.annotation.as_api_json
264
277
  }, status: :conflict
265
278
  end
266
279
 
@@ -106,7 +106,10 @@ module RailsMarkup
106
106
  return redirect_to root_path, alert: "Invalid status for bulk dismiss."
107
107
  end
108
108
 
109
- count = Annotation.where(status: status).update_all(status: "dismissed")
109
+ # Bump revision per row so a stale toolbar edit (which checks baseRevision)
110
+ # conflicts (409) instead of silently overwriting this bulk dismiss.
111
+ count = Annotation.where(status: status)
112
+ .update_all("status = 'dismissed', revision = revision + 1")
110
113
  redirect_to root_path(status: "dismissed"), notice: "#{count} annotations dismissed."
111
114
  end
112
115
 
@@ -122,7 +125,11 @@ module RailsMarkup
122
125
  when "transition"
123
126
  new_status = params[:status]
124
127
  if Annotation::STATUSES.include?(new_status)
125
- @annotation.update!(status: new_status)
128
+ # Lock + bump revision so a concurrent toolbar edit conflicts (409)
129
+ # instead of this board move silently losing to (or clobbering) it.
130
+ @annotation.with_lock do
131
+ @annotation.update!(status: new_status, revision: @annotation.revision + 1)
132
+ end
126
133
  return head :ok
127
134
  else
128
135
  return render json: { error: "invalid status" }, status: :unprocessable_entity
@@ -26,6 +26,10 @@ module RailsMarkup
26
26
  LEGACY_SESSION_PATTERN = /\Arm-[0-9a-f]{16}\z/i
27
27
  LEGACY_CLIENT_ID_LIMIT = 256
28
28
  LEGACY_UUID_NAMESPACE = "265e7cf0-8be6-5e21-8f31-a582cfde8646"
29
+ # Bound thread growth so large or many reply/summary/reason messages (incl.
30
+ # via the MCP tools) can't grow a row without limit.
31
+ MAX_THREAD_ENTRIES = 500
32
+ MAX_THREAD_MESSAGE = 5000
29
33
 
30
34
  # Optional user association — no FK constraint, engine doesn't know host users table
31
35
  belongs_to :user, optional: true
@@ -45,6 +49,7 @@ module RailsMarkup
45
49
  validates :severity, inclusion: { in: SEVERITIES }
46
50
  validates :status, inclusion: { in: STATUSES }
47
51
  validate :thread_must_be_array
52
+ validate :thread_within_limits
48
53
 
49
54
  def self.valid_client_uuid?(value)
50
55
  value.is_a?(String) && CLIENT_UUID_PATTERN.match?(value)
@@ -208,5 +213,15 @@ module RailsMarkup
208
213
  def thread_must_be_array
209
214
  errors.add(:thread, "must be an array") unless thread.is_a?(Array)
210
215
  end
216
+
217
+ def thread_within_limits
218
+ return unless thread.is_a?(Array)
219
+
220
+ errors.add(:thread, "cannot exceed #{MAX_THREAD_ENTRIES} entries") if thread.size > MAX_THREAD_ENTRIES
221
+
222
+ if thread.any? { |entry| entry.is_a?(Hash) && entry["message"].to_s.length > MAX_THREAD_MESSAGE }
223
+ errors.add(:thread, "entry message cannot exceed #{MAX_THREAD_MESSAGE} characters")
224
+ end
225
+ end
211
226
  end
212
227
  end
@@ -22,6 +22,9 @@ module RailsMarkup
22
22
  class ToolError < StandardError; end
23
23
  class TargetError < ToolError; end
24
24
 
25
+ MAX_ACTION_MESSAGE_LENGTH = 5_000
26
+ MAX_ACTION_MESSAGE_BYTES = 5_000
27
+
25
28
  ENV_SCHEMA = {
26
29
  environment: {
27
30
  type: "string",
@@ -74,7 +77,11 @@ module RailsMarkup
74
77
  properties: {
75
78
  action: { type: "string", enum: %w[acknowledge resolve], description: "State transition to apply." },
76
79
  annotationId: { type: "string", description: "The annotation ID" },
77
- summary: { type: "string", description: "Optional resolution summary; valid only for resolve." },
80
+ summary: {
81
+ type: "string",
82
+ maxLength: MAX_ACTION_MESSAGE_LENGTH,
83
+ description: "Optional resolution summary; valid only for resolve."
84
+ },
78
85
  **ENV_SCHEMA
79
86
  },
80
87
  required: %w[action annotationId],
@@ -89,7 +96,7 @@ module RailsMarkup
89
96
  type: "object",
90
97
  properties: {
91
98
  annotationId: { type: "string", description: "The annotation ID" },
92
- message: { type: "string", description: "Reply message" },
99
+ message: { type: "string", maxLength: MAX_ACTION_MESSAGE_LENGTH, description: "Reply message" },
93
100
  **ENV_SCHEMA
94
101
  },
95
102
  required: %w[annotationId message],
@@ -104,7 +111,7 @@ module RailsMarkup
104
111
  type: "object",
105
112
  properties: {
106
113
  annotationId: { type: "string", description: "The annotation ID" },
107
- reason: { type: "string", description: "Reason for dismissal" },
114
+ reason: { type: "string", maxLength: MAX_ACTION_MESSAGE_LENGTH, description: "Reason for dismissal" },
108
115
  **ENV_SCHEMA
109
116
  },
110
117
  required: %w[annotationId reason],
@@ -372,11 +379,14 @@ module RailsMarkup
372
379
  return "action must be acknowledge or resolve." unless %w[acknowledge resolve].include?(args["action"])
373
380
  return "annotationId must be a non-empty string." unless nonempty_string?(args["annotationId"])
374
381
  return "summary must be a string." if args.key?("summary") && !args["summary"].is_a?(String)
382
+ return oversized_action_message_error("summary", args["summary"]) if oversized_action_message?(args["summary"])
375
383
  return "summary is only valid for resolve." if args["action"] != "resolve" && args.key?("summary")
376
384
  when "rails_markup_reply"
377
385
  return "annotationId and message must be non-empty strings." unless nonempty_string?(args["annotationId"]) && nonempty_string?(args["message"])
386
+ return oversized_action_message_error("message", args["message"]) if oversized_action_message?(args["message"])
378
387
  when "rails_markup_dismiss"
379
388
  return "annotationId and reason must be non-empty strings." unless nonempty_string?(args["annotationId"]) && nonempty_string?(args["reason"])
389
+ return oversized_action_message_error("reason", args["reason"]) if oversized_action_message?(args["reason"])
380
390
  when "rails_markup_watch"
381
391
  return "sessionId must be a non-empty string." if args.key?("sessionId") && !nonempty_string?(args["sessionId"])
382
392
  unless !args.key?("timeoutSeconds") || numeric_between?(args["timeoutSeconds"], 0, 300)
@@ -400,6 +410,14 @@ module RailsMarkup
400
410
  value.is_a?(Numeric) && value.finite? && value.between?(minimum, maximum)
401
411
  end
402
412
 
413
+ def oversized_action_message?(value)
414
+ value.is_a?(String) && value.bytesize > MAX_ACTION_MESSAGE_BYTES
415
+ end
416
+
417
+ def oversized_action_message_error(name, _value)
418
+ "#{name} exceeds #{MAX_ACTION_MESSAGE_BYTES} bytes."
419
+ end
420
+
403
421
  def invalid_arguments_response(id, _unknown)
404
422
  tool_error_response(id, "Remove unsupported arguments.")
405
423
  end
@@ -22,6 +22,7 @@ module RailsMarkup
22
22
  MAX_TARGET_BYTES = 16_384
23
23
  MAX_SELECTED_TEXT_BYTES = 2_000
24
24
  MAX_METADATA_BYTES = 65_536
25
+ MAX_THREAD_MESSAGE_BYTES = 5_000
25
26
  INTENTS = %w[fix change question approve].freeze
26
27
  SEVERITIES = %w[suggestion important blocking].freeze
27
28
 
@@ -119,35 +120,64 @@ module RailsMarkup
119
120
  # --- Status transitions ---
120
121
 
121
122
  def acknowledge(annotation_id)
122
- update_status(annotation_id, "acknowledged")
123
+ @mutex.synchronize do
124
+ ann = @annotations_index[annotation_id]
125
+ return nil unless ann
126
+ return ann if ann.status == "acknowledged"
127
+
128
+ validate_transition!(ann, "acknowledged", from: %w[pending])
129
+ ann.status = "acknowledged"
130
+ ann
131
+ end
123
132
  end
124
133
 
125
134
  def resolve(annotation_id, summary: nil)
126
- ann = update_status(annotation_id, "resolved")
127
- return nil unless ann
135
+ changed = false
136
+ ann = @mutex.synchronize do
137
+ annotation = @annotations_index[annotation_id]
138
+ return nil unless annotation
139
+ return annotation if annotation.status == "resolved"
140
+
141
+ validate_transition!(annotation, "resolved", from: %w[pending acknowledged])
142
+ append_thread_message!(annotation, summary) unless summary.nil? || summary == ""
143
+ annotation.status = "resolved"
144
+ changed = true
145
+ annotation
146
+ end
147
+ return ann unless changed
128
148
 
129
- ann.thread << { role: "agent", message: summary, timestamp: Time.now.iso8601 } if summary
130
149
  notify(ann.session_id, type: "annotation_update", annotation: serialize_annotation(ann),
131
150
  status: "resolved", summary: summary)
132
151
  ann
133
152
  end
134
153
 
135
154
  def dismiss(annotation_id, reason: nil)
136
- ann = update_status(annotation_id, "dismissed")
137
- return nil unless ann
155
+ changed = false
156
+ ann = @mutex.synchronize do
157
+ annotation = @annotations_index[annotation_id]
158
+ return nil unless annotation
159
+ return annotation if annotation.status == "dismissed"
160
+
161
+ validate_transition!(annotation, "dismissed", from: %w[pending acknowledged])
162
+ append_thread_message!(annotation, reason) unless reason.nil? || reason == ""
163
+ annotation.status = "dismissed"
164
+ changed = true
165
+ annotation
166
+ end
167
+ return ann unless changed
138
168
 
139
- ann.thread << { role: "agent", message: reason, timestamp: Time.now.iso8601 } if reason
140
169
  notify(ann.session_id, type: "annotation_update", annotation: serialize_annotation(ann),
141
170
  status: "dismissed", reason: reason)
142
171
  ann
143
172
  end
144
173
 
145
174
  def reply(annotation_id, message:)
146
- ann = get_annotation(annotation_id)
147
- return nil unless ann
175
+ ann = @mutex.synchronize do
176
+ annotation = @annotations_index[annotation_id]
177
+ return nil unless annotation
148
178
 
149
- @mutex.synchronize do
150
- ann.thread << { role: "agent", message: message, timestamp: Time.now.iso8601 }
179
+ append_thread_message!(annotation, message)
180
+ annotation
151
181
  end
152
182
  notify(ann.session_id, type: "annotation_update", annotation: serialize_annotation(ann),
153
183
  status: ann.status, message: message)
@@ -201,12 +231,26 @@ module RailsMarkup
201
231
 
202
232
  private
203
233
 
204
- def update_status(annotation_id, new_status)
205
- ann = get_annotation(annotation_id)
206
- return nil unless ann
234
+ def validate_transition!(annotation, new_status, from:)
235
+ return if from.include?(annotation.status)
207
236
 
208
- @mutex.synchronize { ann.status = new_status }
209
- ann
237
+ raise ValidationError, "cannot transition #{annotation.status} annotation to #{new_status}"
238
+ end
239
+
240
+ def append_thread_message!(annotation, message)
241
+ validate_string!("message", message, maximum: MAX_THREAD_MESSAGE_BYTES)
242
+ new_thread = annotation.thread + [{ role: "agent", message: message, timestamp: Time.now.iso8601 }]
243
+ enforce_thread_capacity!(annotation, new_thread)
244
+ annotation.thread = new_thread
245
+ end
246
+
247
+ def enforce_thread_capacity!(annotation, new_thread)
248
+ current_bytes = aggregate_annotation_bytes
249
+ proposed_bytes = current_bytes - annotation_storage_bytes(annotation) +
250
+ annotation_storage_bytes(annotation, thread: new_thread)
251
+ return if proposed_bytes <= @max_annotation_bytes
252
+
253
+ raise CapacityError, "aggregate annotation byte limit of #{@max_annotation_bytes} reached"
210
254
  end
211
255
 
212
256
  def notify(session_id, data)
@@ -235,7 +279,7 @@ module RailsMarkup
235
279
  raise ValidationError, "metadata exceeds #{MAX_METADATA_BYTES} bytes" if metadata_bytes > MAX_METADATA_BYTES
236
280
 
237
281
  JSON.generate(
238
- target:, content:, intent:, severity:, selected_text:, metadata: metadata || {}
282
+ target:, content:, intent:, severity:, selected_text:, metadata: metadata || {}, thread: []
239
283
  ).bytesize
240
284
  rescue JSON::GeneratorError, Encoding::UndefinedConversionError
241
285
  raise ValidationError, "annotation fields must be JSON serializable"
@@ -268,22 +312,27 @@ module RailsMarkup
268
312
  raise CapacityError, "session annotation limit of #{@max_annotations_per_session} reached"
269
313
  end
270
314
 
271
- current_bytes = @sessions.values.sum do |stored_session|
272
- stored_session.annotations.sum { |annotation| annotation_storage_bytes(annotation) }
273
- end
315
+ current_bytes = aggregate_annotation_bytes
274
316
  return if current_bytes + incoming_bytes <= @max_annotation_bytes
275
317
 
276
318
  raise CapacityError, "aggregate annotation byte limit of #{@max_annotation_bytes} reached"
277
319
  end
278
320
 
279
- def annotation_storage_bytes(annotation)
321
+ def aggregate_annotation_bytes
322
+ @sessions.values.sum do |stored_session|
323
+ stored_session.annotations.sum { |annotation| annotation_storage_bytes(annotation) }
324
+ end
325
+ end
326
+
327
+ def annotation_storage_bytes(annotation, thread: annotation.thread)
280
328
  JSON.generate(
281
329
  target: annotation.target,
282
330
  content: annotation.content,
283
331
  intent: annotation.intent,
284
332
  severity: annotation.severity,
285
333
  selected_text: annotation.selected_text,
286
- metadata: annotation.metadata
334
+ metadata: annotation.metadata,
335
+ thread: thread
287
336
  ).bytesize
288
337
  end
289
338
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsMarkup
4
- VERSION = "1.4.0"
4
+ VERSION = "1.4.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-markup
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - InventList