yrb-lite 0.1.0.beta1 → 0.1.0.beta2

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: 73c6ed02c103b7647be2d6052ff660490088bff3e1dba3a82105f8fb42ffecab
4
- data.tar.gz: c308cb9c4e426992b1cbc31b2a870dc3ab3863c3a0217949b7744c4bc4c48d91
3
+ metadata.gz: e81bc59c4870f1c86a73f06fadeaf00fe60b46f0677823d2a6de20190761632e
4
+ data.tar.gz: bf98a231bccd388df0068af60a336c110fdc043daf3cc6cfa56208a4de6352db
5
5
  SHA512:
6
- metadata.gz: 8eb77739b34c860f961a850a70ed39f778f0711d83fda715c8447af1c7d81625472697142ce5aaf8b8a339c408992a6ada2d8a176cf03b1145afc62bd5f7aca5
7
- data.tar.gz: 8886214b02d90bcbe44958deaa0b1b73bd0aa4707f185f34868f5444e99ca8e4ed02dce4100486543fb7dec82d609996de6f90f81acf12fbf1217466115ba985
6
+ metadata.gz: 2e07b28a11faa8ec051b78eb107fa9d4f63fbd483b1118fd7605632665773525cb07f147f3dc25136b3876976b8d9a470db5ff5277a8647cffc64efb8bf4b298
7
+ data.tar.gz: b1afad6a62b1dd0d1edfa7c8bffc5e324d407c489c2ff9fc4056c43d8af0f076ec30fdcf946582ee092b642210414912baa39be279770a88f78279c2ec707127
data/CHANGELOG.md CHANGED
@@ -6,6 +6,34 @@ to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.0.beta2] - 2026-06-16
10
+
11
+ ### Added
12
+
13
+ - Reliable delivery (opt-in, client-driven). A client may tag a document update
14
+ with an `"id"`; the server replies `{ "ack": <id> }` once the update has been
15
+ accepted (recorded in audit mode, applied in fast mode). This lets an
16
+ ack-aware client retain and retransmit an update until delivery is confirmed,
17
+ so an edit can't be silently lost on a flaky connection. Stock clients send no
18
+ `"id"`, never get acks, and behave exactly as before.
19
+ - A vendored, ack-aware `@y-rb/actioncable` provider in the demo
20
+ (`reliable_actioncable_provider.mjs`) that adds reliable delivery with
21
+ "sync-since-last-ack" framing (the unacknowledged tail is sent as one merged,
22
+ causally-complete delta), plus a minimal reference client and an intensive
23
+ message-loss stress test.
24
+
25
+ ### Fixed
26
+
27
+ - Causal-gap protection. The authoritative, fast, and store paths now reject a
28
+ document update that isn't causally ready -- one whose dependencies are
29
+ missing because an earlier update was lost in transit or its durable record
30
+ failed -- and ask the client to resync, instead of recording or relaying an
31
+ un-integrable update that would leave the log permanently pending. Adds native
32
+ `Doc#update_ready?`/`#pending?` (cheap, read-only checks) used to gate the
33
+ record-before-distribute path.
34
+
35
+ ## [0.1.0.beta1]
36
+
9
37
  ### Added
10
38
 
11
39
  - Thread-safe `YrbLite::Doc` and `YrbLite::Awareness` over `yrs` (magnus/rb-sys
@@ -26,4 +54,6 @@ to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
26
54
  - Precompiled native gems for common platforms (no Rust toolchain needed to
27
55
  install) via the cross-gem workflow.
28
56
 
29
- [Unreleased]: https://github.com/jpcamara/yrb-lite/commits/main
57
+ [Unreleased]: https://github.com/jpcamara/yrb-lite/compare/v0.1.0.beta2...main
58
+ [0.1.0.beta2]: https://github.com/jpcamara/yrb-lite/compare/v0.1.0.beta1...v0.1.0.beta2
59
+ [0.1.0.beta1]: https://github.com/jpcamara/yrb-lite/releases/tag/v0.1.0.beta1
data/README.md CHANGED
@@ -292,6 +292,56 @@ mode (in [`examples/actioncable-demo`](examples/actioncable-demo)) wires
292
292
  `on_change` to an fsync'd append-only log and checks, end to end, that the log
293
293
  alone rebuilds the document.
294
294
 
295
+ #### Reliable delivery (acks)
296
+
297
+ The y-websocket protocol is fire-and-forget. If a client's update is lost in
298
+ transit (a flaky socket, a send that never lands) and the client makes no
299
+ further edits, the server stays idle and never asks anyone to resync, so that
300
+ edit is gone -- even though the client believes it was saved. CRDTs converge the
301
+ state everyone *has*; they don't recover an update that never arrived.
302
+
303
+ yrb-lite closes that gap with an opt-in, client-driven acknowledgement. If an
304
+ incoming frame carries an `"id"`, the server replies `{ "ack": <id> }` once the
305
+ update has been **accepted** -- recorded in audit mode, applied in fast mode. A
306
+ causally-gapped update is not acked (it gets a resync instead), so the client
307
+ knows it hasn't landed yet.
308
+
309
+ ```
310
+ client -> server { "m": "<base64 update>", "id": 42 }
311
+ server -> client { "ack": 42 } # update accepted; safe to forget
312
+ ```
313
+
314
+ That's the whole server side. A reliable client tags each outgoing update with
315
+ an incrementing id, keeps it in a pending buffer, and retransmits on a timer (and
316
+ on reconnect) until the matching ack returns. Because CRDT apply is idempotent, a
317
+ resend that already landed is a harmless no-op that just re-acks. An update lost
318
+ in transit is recovered by the client's own retransmit -- no reconnect required,
319
+ and no dependence on a later edit happening to trigger a resync.
320
+
321
+ This is entirely **self-gating**: stock clients send no `"id"`, so they never get
322
+ acks and behave exactly as before. Only a client that opts in by tagging its
323
+ frames participates.
324
+
325
+ Two client examples ship in the demo:
326
+
327
+ - [`frontend/reliable.mjs`](examples/actioncable-demo/frontend/reliable.mjs) — a
328
+ minimal reference client showing the raw mechanism (tag with an id, retain,
329
+ retransmit on a timer, drain on ack), with an end-to-end test that loses an
330
+ update mid-flight and recovers it purely by retransmit.
331
+ - [`frontend/provider/reliable_actioncable_provider.mjs`](examples/actioncable-demo/frontend/provider/reliable_actioncable_provider.mjs)
332
+ — the standard `@y-rb/actioncable` `WebsocketProvider`, vendored and augmented
333
+ for production use. It's a drop-in replacement that speaks the same protocol
334
+ and envelope, and adds reliability with **sync-since-last-ack** framing: rather
335
+ than retransmitting updates one by one, it keeps the unacknowledged local
336
+ updates in a queue and sends their *merge* as a single causally-complete delta,
337
+ with the id being the highest sequence in the batch (so one `{ ack: id }`
338
+ cumulatively confirms everything up to it). Because the whole unacked tail goes
339
+ as one self-contained delta, the server never sees an internal gap and never
340
+ has to round-trip a resync for a lost middle update — the next edit, or the
341
+ next timer tick, carries it. Awareness stays fire-and-forget; against a server
342
+ that doesn't implement acks it warns once and falls back to plain delivery; and
343
+ `reliable: false` opts out entirely. The demo's editor uses this provider.
344
+
295
345
  ### User Awareness/Presence
296
346
 
297
347
  ```ruby
@@ -171,6 +171,24 @@ fn awareness_client_ids_in(bytes: &[u8]) -> Result<Vec<u64>, String> {
171
171
  Ok(ids)
172
172
  }
173
173
 
174
+ /// True if applying `update_bytes` to `doc` would integrate cleanly: every
175
+ /// dependency the update references is already present (the doc's state vector
176
+ /// covers the update's lower bound). A pure read; does not mutate the doc.
177
+ /// When false, applying it would park a pending struct -- the signal that an
178
+ /// earlier, causally-prior update is missing.
179
+ fn update_is_ready(doc: &Doc, update_bytes: &[u8]) -> Result<bool, String> {
180
+ let update = yrs::Update::decode_v1(update_bytes).map_err(|e| e.to_string())?;
181
+ Ok(doc.transact().state_vector() >= update.state_vector_lower())
182
+ }
183
+
184
+ /// True if the doc holds pending structs or a pending delete set -- blocks that
185
+ /// couldn't integrate because a dependency is missing. Used as a backstop after
186
+ /// loading from storage: leftover pending means the stored log has a causal gap.
187
+ fn doc_has_pending(doc: &Doc) -> bool {
188
+ let txn = doc.transact();
189
+ txn.store().pending_update().is_some() || txn.store().pending_ds().is_some()
190
+ }
191
+
174
192
  // ============================================================================
175
193
  // Doc Implementation
176
194
  // ============================================================================
@@ -240,6 +258,22 @@ impl RbDoc {
240
258
  .map_err(runtime_error)
241
259
  }
242
260
 
261
+ /// True if applying `update` would integrate cleanly (its dependencies are
262
+ /// all present). False means it would leave a pending struct -- an earlier
263
+ /// update is missing. Pure read; does not mutate.
264
+ fn update_ready(&self, update: RString) -> Result<bool, Error> {
265
+ let update_bytes = copy_bytes(update);
266
+ let doc = &self.0;
267
+ nogvl(move || update_is_ready(doc, &update_bytes)).map_err(runtime_error)
268
+ }
269
+
270
+ /// True if the document holds pending (un-integrable) structs waiting on a
271
+ /// missing dependency.
272
+ fn pending(&self) -> bool {
273
+ let doc = &self.0;
274
+ nogvl(move || doc_has_pending(doc))
275
+ }
276
+
243
277
  /// Sync step 1: Create a sync message with our state vector
244
278
  fn sync_step1(&self) -> RString {
245
279
  let doc = &self.0;
@@ -318,7 +352,6 @@ impl RbDoc {
318
352
  let msg = Message::Sync(SyncMessage::Update(update_bytes.to_vec()));
319
353
  binary_string(&msg.encode_v1())
320
354
  }
321
-
322
355
  }
323
356
 
324
357
  // ============================================================================
@@ -487,6 +520,22 @@ impl RbAwareness {
487
520
  .map_err(runtime_error)
488
521
  }
489
522
 
523
+ /// True if applying `update` would integrate cleanly (its dependencies are
524
+ /// all present). False means it depends on a missing, causally-prior update.
525
+ /// Pure read; does not mutate.
526
+ fn update_ready(&self, update: RString) -> Result<bool, Error> {
527
+ let update_bytes = copy_bytes(update);
528
+ let doc = self.0.doc();
529
+ nogvl(move || update_is_ready(doc, &update_bytes)).map_err(runtime_error)
530
+ }
531
+
532
+ /// True if the document holds pending (un-integrable) structs waiting on a
533
+ /// missing dependency.
534
+ fn pending(&self) -> bool {
535
+ let doc = self.0.doc();
536
+ nogvl(move || doc_has_pending(doc))
537
+ }
538
+
490
539
  /// Decode the awareness client IDs referenced by a protocol message
491
540
  /// (which may pack several sub-messages together). Sync sub-messages are
492
541
  /// ignored. The ActionCable layer uses this to learn which presence
@@ -577,6 +626,8 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
577
626
  method!(RbDoc::encode_state_as_update, -1),
578
627
  )?;
579
628
  doc_class.define_method("apply_update", method!(RbDoc::apply_update, 1))?;
629
+ doc_class.define_method("update_ready?", method!(RbDoc::update_ready, 1))?;
630
+ doc_class.define_method("pending?", method!(RbDoc::pending, 0))?;
580
631
  doc_class.define_method("sync_step1", method!(RbDoc::sync_step1, 0))?;
581
632
  doc_class.define_method("sync_step2", method!(RbDoc::sync_step2, 1))?;
582
633
  doc_class.define_method(
@@ -606,6 +657,8 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
606
657
  method!(RbAwareness::encode_state_as_update, -1),
607
658
  )?;
608
659
  awareness_class.define_method("apply_update", method!(RbAwareness::apply_update, 1))?;
660
+ awareness_class.define_method("update_ready?", method!(RbAwareness::update_ready, 1))?;
661
+ awareness_class.define_method("pending?", method!(RbAwareness::pending, 0))?;
609
662
  awareness_class.define_method("set_local_state", method!(RbAwareness::set_local_state, 1))?;
610
663
  awareness_class.define_method("local_state", method!(RbAwareness::local_state, 0))?;
611
664
  awareness_class.define_method(
@@ -749,4 +802,50 @@ mod tests {
749
802
  .unwrap()
750
803
  .is_empty());
751
804
  }
805
+
806
+ #[test]
807
+ fn update_readiness_and_pending_detect_a_causal_gap() {
808
+ // Three sequential single-char inserts from one client: A, then B, then
809
+ // C. Each delta depends on the previous, so C can't integrate without B.
810
+ let src = Doc::new();
811
+ let txt = src.get_or_insert_text("t");
812
+ let mut deltas: Vec<Vec<u8>> = Vec::new();
813
+ let mut prev = yrs::StateVector::default();
814
+ for (i, ch) in ["A", "B", "C"].into_iter().enumerate() {
815
+ txt.insert(&mut src.transact_mut(), i as u32, ch);
816
+ deltas.push(src.transact().encode_state_as_update_v1(&prev));
817
+ prev = src.transact().state_vector();
818
+ }
819
+ let (u1, u2, u3) = (&deltas[0], &deltas[1], &deltas[2]);
820
+
821
+ // A doc holding only u1 (u2 was lost in transit / its record failed):
822
+ let doc = Doc::new();
823
+ doc.transact_mut()
824
+ .apply_update(yrs::Update::decode_v1(u1).unwrap())
825
+ .unwrap();
826
+ assert!(update_is_ready(&doc, u1).unwrap(), "u1 has no missing deps");
827
+ assert!(
828
+ !update_is_ready(&doc, u3).unwrap(),
829
+ "u3 depends on the missing u2"
830
+ );
831
+ assert!(
832
+ !doc_has_pending(&doc),
833
+ "nothing pending until u3 is applied"
834
+ );
835
+
836
+ // Applying u3 anyway parks it as a pending struct.
837
+ doc.transact_mut()
838
+ .apply_update(yrs::Update::decode_v1(u3).unwrap())
839
+ .unwrap();
840
+ assert!(
841
+ doc_has_pending(&doc),
842
+ "u3 is pending: its parent u2 is missing"
843
+ );
844
+
845
+ // Once u2 arrives (via resync), u3 integrates and pending clears.
846
+ doc.transact_mut()
847
+ .apply_update(yrs::Update::decode_v1(u2).unwrap())
848
+ .unwrap();
849
+ assert!(!doc_has_pending(&doc), "u2 arrived; u3 integrated");
850
+ }
752
851
  }
data/lib/yrb_lite/sync.rb CHANGED
@@ -128,6 +128,13 @@ module YrbLite
128
128
  # If an `on_change` recorder is registered, document changes take the
129
129
  # strict authoritative path (record -> apply -> broadcast, serialized per
130
130
  # document); otherwise the fast path is used.
131
+ #
132
+ # Reliable delivery (opt-in, client-driven): if the frame carries an "id",
133
+ # the server replies `{ "ack" => id }` once the update has been accepted
134
+ # (recorded in audit mode, applied in fast mode). A causally-gapped update
135
+ # is not acked -- it gets a resync instead -- so an ack-aware client knows
136
+ # to retransmit until the update lands. Stock clients send no "id", never
137
+ # get acks, and are completely unaffected.
131
138
  def sync_receive(data, key = nil)
132
139
  # Pass `key` (params[:id]) when your transport doesn't keep the channel
133
140
  # instance alive across actions. Under AnyCable each RPC command gets a
@@ -139,13 +146,23 @@ module YrbLite
139
146
  m = data.is_a?(Hash) ? (data["m"] || data["update"]) : nil
140
147
  return unless m.is_a?(String)
141
148
 
149
+ # Optional client-supplied id for reliable delivery (see sync_send_ack).
150
+ id = data.is_a?(Hash) ? data["id"] : nil
151
+
142
152
  begin
143
153
  bytes = Base64.strict_decode64(m)
144
154
  rescue ArgumentError
145
155
  return # not valid base64; ignore the frame and keep the connection
146
156
  end
147
157
 
148
- return sync_receive_store_backed(m, bytes) if self.class.sync_backend == :store
158
+ sync_send_ack(id, sync_dispatch(m, bytes))
159
+ end
160
+
161
+ # Route a decoded frame to the backend/path that handles it and return the
162
+ # outcome symbol (:recorded/:applied/:gap/:noop) used by the reliable-
163
+ # delivery ack. A dropped frame returns nil (never acked).
164
+ def sync_dispatch(encoded, bytes)
165
+ return sync_receive_store_backed(encoded, bytes) if self.class.sync_backend == :store
149
166
 
150
167
  awareness = sync_awareness
151
168
  kind = awareness.message_kind(bytes)
@@ -156,9 +173,9 @@ module YrbLite
156
173
  sync_track_clients(awareness, bytes) if kind == MSG_KIND_AWARENESS
157
174
 
158
175
  if kind == MSG_KIND_UPDATE && self.class.on_change
159
- sync_apply_authoritative(awareness, m, bytes)
176
+ sync_apply_authoritative(awareness, encoded, bytes)
160
177
  else
161
- sync_apply_fast(awareness, m, bytes, kind)
178
+ sync_apply_fast(awareness, encoded, bytes, kind)
162
179
  end
163
180
  end
164
181
 
@@ -207,14 +224,37 @@ module YrbLite
207
224
  # Document changes (SyncStep2, Update) and awareness get relayed; requests
208
225
  # (SyncStep1, awareness-query) are answered above and not relayed. An
209
226
  # optional on_save snapshot is taken after a document change.
227
+ #
228
+ # Returns an outcome symbol for the reliable-delivery ack: :applied when a
229
+ # document update was integrated and relayed, :gap when it was rejected for
230
+ # a resync, :noop for everything else (requests, awareness, empty updates).
210
231
  def sync_apply_fast(awareness, encoded, bytes, kind)
232
+ # A document update that isn't causally ready (an earlier one was lost in
233
+ # transit) would relay an un-integrable change to peers and stall the
234
+ # replica. Drop it and ask the client to resync instead, which re-delivers
235
+ # the missing piece. See sync_apply_authoritative for the durable variant.
236
+ if kind == MSG_KIND_UPDATE
237
+ update = awareness.update_from_message(bytes)
238
+ # A no-op message (e.g. the empty SyncStep2 in an opening handshake)
239
+ # carries no change, so there's nothing to relay, persist, or ack.
240
+ return :noop unless update
241
+
242
+ unless awareness.update_ready?(update)
243
+ sync_request_resync(awareness)
244
+ return :gap
245
+ end
246
+ end
247
+
211
248
  response = awareness.handle(bytes)
212
249
  sync_transmit(response) unless response.empty?
213
250
 
214
- return unless [MSG_KIND_UPDATE, MSG_KIND_AWARENESS].include?(kind)
251
+ return :noop unless [MSG_KIND_UPDATE, MSG_KIND_AWARENESS].include?(kind)
215
252
 
216
253
  sync_distribute(encoded)
217
- sync_persist if kind == MSG_KIND_UPDATE
254
+ return :noop unless kind == MSG_KIND_UPDATE
255
+
256
+ sync_persist
257
+ :applied
218
258
  end
219
259
 
220
260
  # Authoritative path: record the change durably, then apply it to the
@@ -224,22 +264,64 @@ module YrbLite
224
264
  # before it has been recorded. If the recorder raises, the change is
225
265
  # rejected (not applied, not broadcast) and the exception propagates, so the
226
266
  # channel can surface it and the client can resync.
267
+ #
268
+ # Before recording, the update must be causally ready: every dependency it
269
+ # references must already be in the doc. If an earlier update was lost in
270
+ # transit, or its record failed, a later update arrives with a gap. Recording
271
+ # it would write a permanently-pending entry to the log -- one that can never
272
+ # be replayed until the missing update shows up. Such an update is rejected
273
+ # (not recorded, not applied, not relayed) and the client is asked to resync,
274
+ # which re-delivers the missing range as one causally-complete delta.
227
275
  def sync_apply_authoritative(awareness, encoded, bytes)
228
276
  recorder = self.class.on_change
229
277
 
230
- modified = Sync.lock_for(@sync_key).synchronize do
278
+ outcome = Sync.lock_for(@sync_key).synchronize do
231
279
  update = awareness.update_from_message(bytes)
232
280
  # A no-op message (e.g. the empty SyncStep2 in a client's opening
233
281
  # handshake) carries no change, so there's nothing to record or relay.
234
- next false unless update
282
+ next :noop unless update
283
+ next :gap unless awareness.update_ready?(update)
235
284
 
236
285
  recorder.call(@sync_key, update) # durable write; raise to reject
237
286
  awareness.apply_update(update) # only recorded changes reach the doc
238
287
  sync_distribute(encoded) # ...and only then the wire
239
- true
288
+ :recorded
289
+ end
290
+
291
+ case outcome
292
+ when :recorded then sync_persist
293
+ when :gap then sync_request_resync(awareness)
240
294
  end
241
295
 
242
- sync_persist if modified
296
+ # Surface the outcome for the reliable-delivery ack: :recorded means the
297
+ # update is durably written (and will be acked); :gap triggered a resync
298
+ # (no ack); :noop carried no change.
299
+ outcome
300
+ end
301
+
302
+ # Ask this connection's client to resync: re-send SyncStep1 carrying the
303
+ # server's current (gap-free) state vector. The client replies SyncStep2
304
+ # with everything the server is missing, delivered as one causally-complete
305
+ # delta -- which heals the gap that triggered the resync.
306
+ def sync_request_resync(awareness)
307
+ sync_transmit(awareness.sync_step1)
308
+ end
309
+
310
+ # Reliable delivery: acknowledge an accepted update back to the sending
311
+ # connection. An ack-aware client tags each outgoing update with an "id"
312
+ # and retains it until the matching `{ "ack" => id }` returns, retransmitting
313
+ # on a timer or reconnect; idempotent CRDT apply makes resends free. We ack
314
+ # only when the client supplied an id (so stock clients are unaffected) and
315
+ # the update was actually accepted -- recorded in audit mode, applied in fast
316
+ # mode. A gapped update gets no ack (it got a resync), so the client keeps
317
+ # retransmitting until the missing range lands and the update can integrate.
318
+ def sync_send_ack(id, outcome)
319
+ return if id.nil?
320
+ return unless %i[recorded applied].include?(outcome)
321
+
322
+ # Braces are load-bearing: a bare hash would bind to transmit's `via:`
323
+ # keyword instead of its positional data argument.
324
+ transmit({ "ack" => id })
243
325
  end
244
326
 
245
327
  # Single broadcast point for both paths (and presence removal), so the
@@ -314,19 +396,40 @@ module YrbLite
314
396
  # changes are recorded durably before relay and then broadcast, and
315
397
  # awareness is relayed best-effort. Echoing back to the sender is harmless,
316
398
  # since the CRDT apply is idempotent.
399
+ #
400
+ # Returns an outcome symbol for the reliable-delivery ack: :recorded when a
401
+ # document update was durably recorded and relayed, :gap when it was
402
+ # rejected for a resync, :noop for everything else.
317
403
  def sync_receive_store_backed(encoded, bytes)
318
404
  case Sync.codec.message_kind(bytes)
319
405
  when MSG_KIND_SYNC_STEP1
320
406
  result = sync_load_doc.handle_sync_message(bytes)
321
407
  sync_transmit(result[2]) if result
408
+ :noop
322
409
  when MSG_KIND_UPDATE
323
410
  update = Sync.codec.update_from_message(bytes)
324
- return unless update
411
+ return :noop unless update
412
+
413
+ # Store mode keeps no warm replica, so to tell whether this update is
414
+ # causally ready we rebuild the doc from the store and check against it.
415
+ # That's an O(history) load per update (mitigated by snapshotting the
416
+ # store on the load path). A gappy update -- an earlier one was lost or
417
+ # its record failed -- is rejected and the client asked to resync,
418
+ # rather than written to the log as a permanently-pending entry.
419
+ doc = sync_load_doc
420
+ unless doc.update_ready?(update)
421
+ sync_transmit(doc.sync_step1)
422
+ return :gap
423
+ end
325
424
 
326
425
  self.class.on_change&.call(@sync_key, update) # record before relay
327
426
  sync_distribute(encoded)
427
+ :recorded
328
428
  when MSG_KIND_AWARENESS
329
429
  sync_distribute(encoded)
430
+ :noop
431
+ else
432
+ :noop
330
433
  end
331
434
  end
332
435
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module YrbLite
4
- VERSION = "0.1.0.beta1"
4
+ VERSION = "0.1.0.beta2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yrb-lite
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.beta1
4
+ version: 0.1.0.beta2
5
5
  platform: ruby
6
6
  authors:
7
7
  - JP Camara