async-matrix 3.0.0-aarch64-linux → 3.0.1-aarch64-linux

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.
@@ -0,0 +1,482 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the Apache License, Version 2.0.
4
+ # Copyright, 2026, by General Intelligence Systems.
5
+
6
+ require_relative "error"
7
+ require_relative "event"
8
+ require_relative "encrypted_message"
9
+
10
+ module Protocol
11
+ module Matrix
12
+ # One batch of messages, read exactly once.
13
+ #
14
+ # A homeserver hands over messages in batches, never one at a time: a /sync
15
+ # response or an application service transaction arrives whole, carrying
16
+ # to-device messages and room timelines together. This is that batch, turned
17
+ # into a single-pass stream — #read gives the next message or nil, in the
18
+ # shape Protocol::GRPC::Body::Readable uses for gRPC frames.
19
+ #
20
+ # batch = MessageBatch.from_sync(response)
21
+ #
22
+ # while message = batch.read
23
+ # message.to_device? # plumbing (room keys) or timeline?
24
+ # message.encrypted? # needs a session before it can be read?
25
+ # end
26
+ #
27
+ # batch.next_batch # the cursor this batch ends at
28
+ #
29
+ # NOT REWINDABLE, AND THAT IS THE POINT. Reading a message is not free:
30
+ # decrypting one ratchets a session forward, so "read it again" is not a
31
+ # thing the protocol can offer. A drained batch answers nil forever, and
32
+ # #each consumes rather than iterating a collection. If a consumer needs to
33
+ # keep a message, it keeps the message — not the batch.
34
+ #
35
+ # TO-DEVICE MESSAGES COME FIRST, always. A batch routinely carries both an
36
+ # `m.room_key` and the timeline event that key unlocks, and the order they
37
+ # are read in decides whether that event decrypts on the first pass or is
38
+ # stored as a placeholder and repaired later. Making the stream ordered is
39
+ # what takes that decision away from the caller and from any scheduler: a
40
+ # timeline message is unreachable until the to-device messages ahead of it
41
+ # have been read.
42
+ #
43
+ # The two wire formats differ, and only in how they are unpacked:
44
+ #
45
+ # /sync to_device.events, then rooms.{join,invite,leave}
46
+ # AS transaction de.sorunome.msc2409.to_device, then events
47
+ #
48
+ # So they get two constructors and produce one stream, which is why an
49
+ # appservice and a syncing client can share everything downstream of here.
50
+ class MessageBatch
51
+ # Where a message sat in the batch it came from. Carried because a
52
+ # consumer projecting rooms needs to know that an event arrived as invite
53
+ # state rather than timeline, which the event itself does not say.
54
+ SECTIONS = %i[to_device join invite leave transaction].freeze
55
+
56
+ class Error < Protocol::Matrix::Error; end
57
+
58
+ # #read after the batch was drained is fine (nil). This is for a consumer
59
+ # that asks for a second pass over something already consumed.
60
+ class ConsumedError < Error; end
61
+
62
+ # Unpack a /sync response.
63
+ #
64
+ # @parameter response [Hash] the parsed response body, string keys.
65
+ def self.from_sync(response)
66
+ messages = []
67
+
68
+ to_device = response.dig("to_device", "events") || []
69
+ to_device.each { |event| messages << build(event, :to_device, nil) }
70
+
71
+ rooms = response["rooms"] || {}
72
+
73
+ (rooms["join"] || {}).each do |room_id, data|
74
+ timeline = data.dig("timeline", "events") || []
75
+ timeline.each { |event| messages << build(event, :join, room_id) }
76
+ end
77
+
78
+ # Invites carry stripped state and no timeline, but it is the only thing
79
+ # that says who invited us, so dropping it loses the invite itself.
80
+ (rooms["invite"] || {}).each do |room_id, data|
81
+ state = data.dig("invite_state", "events") || []
82
+ state.each { |event| messages << build(event, :invite, room_id) }
83
+ end
84
+
85
+ (rooms["leave"] || {}).each do |room_id, data|
86
+ timeline = data.dig("timeline", "events") || []
87
+ timeline.each { |event| messages << build(event, :leave, room_id) }
88
+ end
89
+
90
+ new(messages, next_batch: response["next_batch"])
91
+ end
92
+
93
+ # Unpack an application service transaction.
94
+ #
95
+ # To-device messages ride the MSC4203 key, which Synapse still sends under
96
+ # its original MSC2409 name; the stable spelling is accepted first so this
97
+ # keeps working when that changes.
98
+ #
99
+ # @parameter transaction [Hash] the parsed PUT body, string keys.
100
+ # @parameter txn_id [String] the transaction id, which is this batch's
101
+ # cursor — the appservice equivalent of next_batch.
102
+ def self.from_transaction(transaction, txn_id: nil)
103
+ messages = []
104
+
105
+ to_device = transaction["to_device"] ||
106
+ transaction["de.sorunome.msc2409.to_device"] || []
107
+ to_device.each { |event| messages << build(event, :to_device, nil) }
108
+
109
+ # Transaction events carry their own room_id; there is no per-room
110
+ # grouping to unpack.
111
+ (transaction["events"] || []).each do |event|
112
+ messages << build(event, :transaction, event["room_id"])
113
+ end
114
+
115
+ new(messages, next_batch: txn_id)
116
+ end
117
+
118
+ # An encrypted event becomes an EncryptedMessage, anything else an Event.
119
+ # Both answer #type, #content, #encrypted?, #decrypted? and #to_device?,
120
+ # so a consumer reads either without asking which it has.
121
+ def self.build(event, section, room_id)
122
+ # A to-device event has no room_id of its own; a /sync room section
123
+ # knows the room its events belong to, and the events themselves may
124
+ # omit it. Supplying it keeps #to_device? honest either way.
125
+ if room_id && event["room_id"].nil?
126
+ event = event.merge("room_id" => room_id)
127
+ end
128
+
129
+ if EncryptedMessage.encrypted?(event)
130
+ [EncryptedMessage.new(event), section]
131
+ else
132
+ [Event.new(event), section]
133
+ end
134
+ end
135
+
136
+ def initialize(messages, next_batch: nil)
137
+ @messages = messages
138
+ @next_batch = next_batch
139
+ @position = 0
140
+ @section = nil
141
+ end
142
+
143
+ # The cursor this batch ends at: `next_batch` from /sync, the transaction
144
+ # id from an appservice.
145
+ #
146
+ # COMMIT IT ONLY ONCE THE BATCH IS DRAINED AND ITS EFFECTS ARE DURABLE.
147
+ # Storing it earlier means a crash loses every message still in this
148
+ # batch, with nothing anywhere recording that they were missed.
149
+ attr_reader :next_batch
150
+
151
+ # How many messages the batch held. Fixed at construction — it does not
152
+ # count down as the batch is read.
153
+ def size = @messages.length
154
+
155
+ # Have all of them been read?
156
+ def drained? = @position >= @messages.length
157
+
158
+ # The next message, or nil once drained. To-device messages first.
159
+ def read
160
+ if drained?
161
+ nil
162
+ else
163
+ message, section = @messages[@position]
164
+ @position += 1
165
+ @section = section
166
+ message
167
+ end
168
+ end
169
+
170
+ # The section the message most recently returned by #read came from, one
171
+ # of SECTIONS. Read it after #read, not before.
172
+ attr_reader :section
173
+
174
+ # Did the last message read arrive as to-device rather than from a room?
175
+ #
176
+ # ON THE BATCH, NOT THE MESSAGE, because the batch is the only thing that
177
+ # knows. Asking the message would mean inferring it from a missing
178
+ # room_id, and that is wrong twice over: a transaction timeline event may
179
+ # omit the room_id the section supplies, and inference would call it
180
+ # to-device. The section is stated by the wire format; nothing has to
181
+ # guess.
182
+ def to_device? = @section == :to_device
183
+
184
+ # Read every remaining message. CONSUMES the batch: this is a drain, not
185
+ # an iteration, and a second call yields nothing because there is nothing
186
+ # left rather than because iteration restarted.
187
+ def each
188
+ unless block_given?
189
+ raise ConsumedError, "MessageBatch#each requires a block; a batch is not enumerable"
190
+ end
191
+
192
+ while (message = read)
193
+ yield(message, @section)
194
+ end
195
+
196
+ self
197
+ end
198
+ end
199
+ end
200
+ end
201
+
202
+ __END__
203
+ describe "Protocol::Matrix::MessageBatch" do
204
+ def to_device_event
205
+ {
206
+ "type" => "m.room.encrypted",
207
+ "sender" => "@bob:example.org",
208
+ "content" => {
209
+ "algorithm" => "m.olm.v1.curve25519-aes-sha2",
210
+ "sender_key" => "senderkey",
211
+ "ciphertext" => {"ourkey" => {"type" => 0, "body" => "body"}},
212
+ },
213
+ }
214
+ end
215
+
216
+ def encrypted_timeline_event(event_id = "$enc1")
217
+ {
218
+ "type" => "m.room.encrypted",
219
+ "event_id" => event_id,
220
+ "sender" => "@alice:example.org",
221
+ "origin_server_ts" => 1,
222
+ "content" => {
223
+ "algorithm" => "m.megolm.v1.aes-sha2",
224
+ "ciphertext" => "AwgAEnAC",
225
+ "session_id" => "session1",
226
+ },
227
+ }
228
+ end
229
+
230
+ def plaintext_event(event_id = "$msg1")
231
+ {
232
+ "type" => "m.room.message",
233
+ "event_id" => event_id,
234
+ "sender" => "@alice:example.org",
235
+ "origin_server_ts" => 2,
236
+ "content" => {"msgtype" => "m.text", "body" => "hello"},
237
+ }
238
+ end
239
+
240
+ def sync_response(extra = {})
241
+ {
242
+ "next_batch" => "s72_1234",
243
+ "to_device" => {"events" => [to_device_event]},
244
+ "rooms" => {
245
+ "join" => {
246
+ "!room:example.org" => {
247
+ "timeline" => {"events" => [encrypted_timeline_event, plaintext_event]},
248
+ },
249
+ },
250
+ },
251
+ }.merge(extra)
252
+ end
253
+
254
+ # ── Unpacking /sync ───────────────────────────────────────────────────────
255
+
256
+ it "reads every message in the batch, then nil" do
257
+ batch = Protocol::Matrix::MessageBatch.from_sync(sync_response)
258
+
259
+ batch.size.should == 3
260
+ batch.read.should.be.kind_of Protocol::Matrix::EncryptedMessage
261
+ batch.read.should.be.kind_of Protocol::Matrix::EncryptedMessage
262
+ batch.read.should.be.kind_of Protocol::Matrix::Event
263
+ batch.read.should.be.nil
264
+ batch.drained?.should == true
265
+ end
266
+
267
+ # THE ORDERING GUARANTEE. A batch carrying both a room key and the event it
268
+ # unlocks must hand over the key first, or the event decrypts as a
269
+ # placeholder on the first pass.
270
+ it "yields to-device messages before any timeline message" do
271
+ batch = Protocol::Matrix::MessageBatch.from_sync(sync_response)
272
+
273
+ batch.read
274
+ batch.to_device?.should == true
275
+ batch.section.should == :to_device
276
+
277
+ batch.read
278
+ batch.to_device?.should == false
279
+ batch.section.should == :join
280
+ end
281
+
282
+ it "carries the cursor the batch ends at" do
283
+ Protocol::Matrix::MessageBatch.from_sync(sync_response).next_batch.should == "s72_1234"
284
+ end
285
+
286
+ it "builds an EncryptedMessage for an encrypted event and an Event otherwise" do
287
+ batch = Protocol::Matrix::MessageBatch.from_sync(sync_response)
288
+ batch.read
289
+
290
+ encrypted = batch.read
291
+ encrypted.encrypted?.should == true
292
+ encrypted.decrypted?.should == false
293
+ encrypted.session_id.should == "session1"
294
+
295
+ plain = batch.read
296
+ plain.encrypted?.should == false
297
+ plain.decrypted?.should == true
298
+ plain.type.should == "m.room.message"
299
+ plain.content.body.should == "hello"
300
+ end
301
+
302
+ # A /sync room section knows the room; the events inside it may not repeat it.
303
+ it "attributes a timeline event to its room" do
304
+ batch = Protocol::Matrix::MessageBatch.from_sync(sync_response)
305
+ batch.read
306
+
307
+ batch.read.room_id.should == "!room:example.org"
308
+ end
309
+
310
+ it "leaves a to-device message without a room" do
311
+ batch = Protocol::Matrix::MessageBatch.from_sync(sync_response)
312
+
313
+ batch.read.room_id.should.be.nil
314
+ batch.to_device?.should == true
315
+ end
316
+
317
+ it "does not overwrite a room_id the event already carries" do
318
+ event = encrypted_timeline_event.merge("room_id" => "!actual:example.org")
319
+ response = {
320
+ "rooms" => {"join" => {"!section:example.org" => {"timeline" => {"events" => [event]}}}},
321
+ }
322
+
323
+ Protocol::Matrix::MessageBatch.from_sync(response).read.room_id.should == "!actual:example.org"
324
+ end
325
+
326
+ it "reads invite state, which is the only record of the invite" do
327
+ response = {
328
+ "rooms" => {
329
+ "invite" => {
330
+ "!invited:example.org" => {
331
+ "invite_state" => {
332
+ "events" => [{
333
+ "type" => "m.room.member",
334
+ "sender" => "@carol:example.org",
335
+ "state_key" => "@us:example.org",
336
+ "content" => {"membership" => "invite"},
337
+ }],
338
+ },
339
+ },
340
+ },
341
+ },
342
+ }
343
+ batch = Protocol::Matrix::MessageBatch.from_sync(response)
344
+ message = batch.read
345
+
346
+ message.type.should == "m.room.member"
347
+ message.room_id.should == "!invited:example.org"
348
+ batch.section.should == :invite
349
+ end
350
+
351
+ it "reads the timeline of a room we left" do
352
+ response = {
353
+ "rooms" => {
354
+ "leave" => {"!gone:example.org" => {"timeline" => {"events" => [plaintext_event]}}},
355
+ },
356
+ }
357
+ batch = Protocol::Matrix::MessageBatch.from_sync(response)
358
+ batch.read
359
+
360
+ batch.section.should == :leave
361
+ end
362
+
363
+ it "handles an empty sync with nothing in it" do
364
+ batch = Protocol::Matrix::MessageBatch.from_sync({"next_batch" => "s1"})
365
+
366
+ batch.size.should == 0
367
+ batch.drained?.should == true
368
+ batch.read.should.be.nil
369
+ batch.next_batch.should == "s1"
370
+ end
371
+
372
+ # ── Unpacking an appservice transaction ───────────────────────────────────
373
+
374
+ it "unpacks a transaction into the same stream" do
375
+ transaction = {
376
+ "de.sorunome.msc2409.to_device" => [to_device_event],
377
+ "events" => [encrypted_timeline_event.merge("room_id" => "!room:example.org")],
378
+ }
379
+ batch = Protocol::Matrix::MessageBatch.from_transaction(transaction, txn_id: "txn42")
380
+
381
+ batch.size.should == 2
382
+ batch.read
383
+ batch.to_device?.should == true
384
+ batch.section.should == :to_device
385
+
386
+ timeline = batch.read
387
+ timeline.room_id.should == "!room:example.org"
388
+ batch.section.should == :transaction
389
+ batch.next_batch.should == "txn42"
390
+ end
391
+
392
+ # Synapse sends the unstable MSC2409 spelling today; the stable MSC4203 key
393
+ # is preferred so this keeps working when that lands.
394
+ it "prefers the stable to_device key over the unstable one" do
395
+ transaction = {
396
+ "to_device" => [to_device_event],
397
+ "de.sorunome.msc2409.to_device" => [to_device_event, to_device_event],
398
+ }
399
+
400
+ Protocol::Matrix::MessageBatch.from_transaction(transaction).size.should == 1
401
+ end
402
+
403
+ it "handles a transaction with no to-device messages" do
404
+ transaction = {"events" => [plaintext_event]}
405
+ batch = Protocol::Matrix::MessageBatch.from_transaction(transaction)
406
+
407
+ batch.size.should == 1
408
+ batch.read.type.should == "m.room.message"
409
+ batch.to_device?.should == false
410
+ batch.section.should == :transaction
411
+ end
412
+
413
+ it "handles an empty transaction" do
414
+ batch = Protocol::Matrix::MessageBatch.from_transaction({})
415
+
416
+ batch.size.should == 0
417
+ batch.read.should.be.nil
418
+ end
419
+
420
+ # ── Read exactly once ─────────────────────────────────────────────────────
421
+
422
+ # Decrypting ratchets a session forward, so "read it again" is not something
423
+ # the protocol can offer. A drained batch stays drained.
424
+ it "cannot be rewound" do
425
+ batch = Protocol::Matrix::MessageBatch.from_sync(sync_response)
426
+ 3.times { batch.read }
427
+
428
+ batch.read.should.be.nil
429
+ batch.read.should.be.nil
430
+ batch.drained?.should == true
431
+ end
432
+
433
+ it "consumes the batch when drained with each" do
434
+ batch = Protocol::Matrix::MessageBatch.from_sync(sync_response)
435
+ seen = []
436
+ batch.each { |message| seen << message }
437
+
438
+ seen.length.should == 3
439
+ batch.drained?.should == true
440
+
441
+ again = []
442
+ batch.each { |message| again << message }
443
+ again.should == []
444
+ end
445
+
446
+ it "reports the section alongside each message in each" do
447
+ batch = Protocol::Matrix::MessageBatch.from_sync(sync_response)
448
+ sections = []
449
+ batch.each { |_message, section| sections << section }
450
+
451
+ sections.should == [:to_device, :join, :join]
452
+ end
453
+
454
+ it "continues a partly read batch rather than restarting" do
455
+ batch = Protocol::Matrix::MessageBatch.from_sync(sync_response)
456
+ batch.read
457
+
458
+ remaining = []
459
+ batch.each { |message| remaining << message }
460
+
461
+ remaining.length.should == 2
462
+ end
463
+
464
+ # A batch is a stream, not a collection: there is no non-consuming read, so
465
+ # #each without a block has nothing sensible to return.
466
+ it "refuses to be treated as an enumerable" do
467
+ batch = Protocol::Matrix::MessageBatch.from_sync(sync_response)
468
+
469
+ lambda { batch.each }.should.raise(Protocol::Matrix::MessageBatch::ConsumedError)
470
+ end
471
+
472
+ it "keeps size fixed as it is read" do
473
+ batch = Protocol::Matrix::MessageBatch.from_sync(sync_response)
474
+ batch.read
475
+
476
+ batch.size.should == 3
477
+ end
478
+
479
+ it "has no section before anything has been read" do
480
+ Protocol::Matrix::MessageBatch.from_sync(sync_response).section.should.be.nil
481
+ end
482
+ end
@@ -8,7 +8,7 @@ require "json"
8
8
  require "json_schemer"
9
9
  require "pathname"
10
10
 
11
- module Async
11
+ module Protocol
12
12
  module Matrix
13
13
  module Schema
14
14
  # Loads and indexes Matrix event schemas from the official matrix-org/matrix-spec
@@ -18,7 +18,7 @@ module Async
18
18
  # process. Each schema is a JSONSchemer::Schema instance that resolves relative
19
19
  # $ref paths (e.g. core-event-schema/room_event.yaml) via a file-based resolver.
20
20
  #
21
- # registry = Async::Matrix::Schema::Registry.instance
21
+ # registry = Protocol::Matrix::Schema::Registry.instance
22
22
  # registry["m.room.message"] # => JSONSchemer::Schema
23
23
  # registry.variant("m.room.message", "m.text") # => JSONSchemer::Schema
24
24
  # registry.event_types # => ["m.accepted_terms", ...]
@@ -262,13 +262,13 @@ module Async
262
262
  end
263
263
 
264
264
  __END__
265
- describe "Async::Matrix::Schema::Registry" do
265
+ describe "Protocol::Matrix::Schema::Registry" do
266
266
  it "loads schemas from disk" do
267
- Async::Matrix::Schema::Registry.instance.size.should.be > 0
267
+ Protocol::Matrix::Schema::Registry.instance.size.should.be > 0
268
268
  end
269
269
 
270
270
  it "has known event types" do
271
- types = Async::Matrix::Schema::Registry.instance.event_types
271
+ types = Protocol::Matrix::Schema::Registry.instance.event_types
272
272
  types.should.include "m.room.message"
273
273
  types.should.include "m.room.member"
274
274
  types.should.include "m.room.create"
@@ -276,21 +276,21 @@ __END__
276
276
  end
277
277
 
278
278
  it "returns a JSONSchemer::Schema for a known type" do
279
- schema = Async::Matrix::Schema::Registry.instance["m.room.message"]
279
+ schema = Protocol::Matrix::Schema::Registry.instance["m.room.message"]
280
280
  schema.should.not.be.nil
281
281
  schema.should.be.kind_of JSONSchemer::Schema
282
282
  end
283
283
 
284
284
  it "returns nil for an unknown type" do
285
- Async::Matrix::Schema::Registry.instance["m.fake.event"].should.be.nil
285
+ Protocol::Matrix::Schema::Registry.instance["m.fake.event"].should.be.nil
286
286
  end
287
287
 
288
288
  it "has variant schemas" do
289
- Async::Matrix::Schema::Registry.instance.variant_types.should.not.be.empty
289
+ Protocol::Matrix::Schema::Registry.instance.variant_types.should.not.be.empty
290
290
  end
291
291
 
292
292
  it "resolves variant schemas" do
293
- schema = Async::Matrix::Schema::Registry.instance.variant("m.room.message", "m.text")
293
+ schema = Protocol::Matrix::Schema::Registry.instance.variant("m.room.message", "m.text")
294
294
  schema.should.not.be.nil
295
295
  schema.should.be.kind_of JSONSchemer::Schema
296
296
  end
@@ -304,7 +304,7 @@ __END__
304
304
  "origin_server_ts" => 1234567890,
305
305
  "room_id" => "!room:example.org"
306
306
  }
307
- Async::Matrix::Schema::Registry.instance.valid?(event).should == true
307
+ Protocol::Matrix::Schema::Registry.instance.valid?(event).should == true
308
308
  end
309
309
 
310
310
  it "rejects m.room.message missing body" do
@@ -316,7 +316,7 @@ __END__
316
316
  "origin_server_ts" => 1234567890,
317
317
  "room_id" => "!room:example.org"
318
318
  }
319
- Async::Matrix::Schema::Registry.instance.valid?(event).should == false
319
+ Protocol::Matrix::Schema::Registry.instance.valid?(event).should == false
320
320
  end
321
321
 
322
322
  it "is lenient with unknown event types" do
@@ -328,7 +328,7 @@ __END__
328
328
  "origin_server_ts" => 1234567890,
329
329
  "room_id" => "!room:example.org"
330
330
  }
331
- Async::Matrix::Schema::Registry.instance.valid?(event).should == true
331
+ Protocol::Matrix::Schema::Registry.instance.valid?(event).should == true
332
332
  end
333
333
 
334
334
  it "validates m.room.member with correct membership" do
@@ -341,7 +341,7 @@ __END__
341
341
  "origin_server_ts" => 1234567890,
342
342
  "room_id" => "!room:example.org"
343
343
  }
344
- Async::Matrix::Schema::Registry.instance.valid?(event).should == true
344
+ Protocol::Matrix::Schema::Registry.instance.valid?(event).should == true
345
345
  end
346
346
 
347
347
  it "rejects m.room.member with invalid membership" do
@@ -354,17 +354,17 @@ __END__
354
354
  "origin_server_ts" => 1234567890,
355
355
  "room_id" => "!room:example.org"
356
356
  }
357
- Async::Matrix::Schema::Registry.instance.valid?(event).should == false
357
+ Protocol::Matrix::Schema::Registry.instance.valid?(event).should == false
358
358
  end
359
359
 
360
360
  it "returns content properties for known types" do
361
- props = Async::Matrix::Schema::Registry.instance.content_properties("m.room.message")
361
+ props = Protocol::Matrix::Schema::Registry.instance.content_properties("m.room.message")
362
362
  props.should.include "msgtype"
363
363
  props.should.include "body"
364
364
  end
365
365
 
366
366
  it "returns empty content properties for unknown types" do
367
- Async::Matrix::Schema::Registry.instance.content_properties("m.fake.type").should == []
367
+ Protocol::Matrix::Schema::Registry.instance.content_properties("m.fake.type").should == []
368
368
  end
369
369
 
370
370
  it "performs variant validation for m.room.message with msgtype" do
@@ -376,6 +376,6 @@ __END__
376
376
  "origin_server_ts" => 1234567890,
377
377
  "room_id" => "!room:example.org"
378
378
  }
379
- Async::Matrix::Schema::Registry.instance.validate(event).should == []
379
+ Protocol::Matrix::Schema::Registry.instance.validate(event).should == []
380
380
  end
381
381
  end