twilic 3.0.0

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,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module Twilic
6
+ module Core
7
+ module Dictionary
8
+ module_function
9
+
10
+ def decode_trained_dictionary_payload(payload)
11
+ reader = Wire::Reader.new(payload)
12
+ n = reader.read_varuint
13
+ values = []
14
+ n.times do
15
+ values << reader.read_string
16
+ end
17
+ raise Errors.invalid_data("trained dictionary payload trailing bytes") unless reader.eof?
18
+
19
+ values
20
+ end
21
+
22
+ def encode_trained_dictionary_block(values, dictionary)
23
+ if values.empty?
24
+ out = +""
25
+ out << "\x00"
26
+ Wire.encode_varuint(0, out)
27
+ return [out, true]
28
+ end
29
+ by_value = {}
30
+ dictionary.each_with_index { |v, idx| by_value[v] = idx }
31
+ ids = values.map do |value|
32
+ id = by_value[value]
33
+ return [nil, false] unless id
34
+
35
+ id
36
+ end
37
+ raw = +""
38
+ raw << "\x00"
39
+ Wire.encode_varuint(ids.length, raw)
40
+ ids.each { |id| Wire.encode_varuint(id, raw) }
41
+ max_id = ids.max || 0
42
+ bit_width = max_id.zero? ? 0 : (64 - max_id.to_s(2).length)
43
+ packed = +""
44
+ pack_fixed_width_u64(ids, bit_width, packed)
45
+ bitpacked = +""
46
+ bitpacked << "\x01"
47
+ Wire.encode_varuint(ids.length, bitpacked)
48
+ bitpacked << bit_width.chr
49
+ bitpacked << packed
50
+ return [bitpacked, true] if bitpacked.bytesize < raw.bytesize
51
+
52
+ [raw, true]
53
+ end
54
+
55
+ def decode_trained_dictionary_block(block, dictionary)
56
+ reader = Wire::Reader.new(block)
57
+ mode = reader.read_u8
58
+ n = reader.read_varuint
59
+ ids = case mode
60
+ when 0
61
+ Array.new(n) { reader.read_varuint }
62
+ when 1
63
+ bit_width = reader.read_u8
64
+ remaining = block.bytesize - reader.position
65
+ packed = reader.read_exact(remaining)
66
+ unpack_fixed_width_u64(packed, n, bit_width)
67
+ else
68
+ raise Errors.invalid_data("trained dictionary block mode")
69
+ end
70
+ raise Errors.invalid_data("trained dictionary block trailing bytes") unless reader.eof?
71
+
72
+ ids.map do |id|
73
+ raise Errors.invalid_data("trained dictionary block id") if id >= dictionary.length
74
+
75
+ dictionary[id]
76
+ end
77
+ end
78
+
79
+ WideU128 = Data.define(:lo, :hi)
80
+
81
+ def wide_from_u64(v)
82
+ WideU128.new(lo: v, hi: 0)
83
+ end
84
+
85
+ def wide_mask(width)
86
+ if width == 64
87
+ WideU128.new(lo: 0xFFFFFFFFFFFFFFFF, hi: 0xFFFFFFFFFFFFFFFF)
88
+ elsif width.zero?
89
+ WideU128.new(lo: 0, hi: 0)
90
+ elsif width <= 64
91
+ WideU128.new(lo: (1 << width) - 1, hi: 0)
92
+ else
93
+ WideU128.new(lo: 0xFFFFFFFFFFFFFFFF, hi: (1 << (width - 64)) - 1)
94
+ end
95
+ end
96
+
97
+ def pack_fixed_width_u64(values, width, out)
98
+ raise Errors.invalid_data("fixed-width u64 bit width") if width > 64
99
+
100
+ if width.zero?
101
+ values.each do |value|
102
+ raise Errors.invalid_data("fixed-width u64 value overflow") unless value.zero?
103
+ end
104
+ return
105
+ end
106
+ acc = WideU128.new(lo: 0, hi: 0)
107
+ acc_bits = 0
108
+ values.each do |value|
109
+ raise Errors.invalid_data("fixed-width u64 value overflow") if width < 64 && (value >> width) != 0
110
+
111
+ acc = wide_or(acc, wide_shl(wide_from_u64(value), acc_bits))
112
+ acc_bits += width
113
+ while acc_bits >= 8
114
+ out << (acc.lo & 0xFF).chr
115
+ acc = wide_shr(acc, 8)
116
+ acc_bits -= 8
117
+ end
118
+ end
119
+ out << (acc.lo & 0xFF).chr if acc_bits.positive?
120
+ end
121
+
122
+ def unpack_fixed_width_u64(bytes, count, width)
123
+ raise Errors.invalid_data("fixed-width u64 bit width") if width > 64
124
+
125
+ if width.zero?
126
+ bytes.each { |b| raise Errors.invalid_data("fixed-width u64 trailing bytes") unless b.zero? }
127
+ return Array.new(count, 0)
128
+ end
129
+ out = []
130
+ acc = WideU128.new(lo: 0, hi: 0)
131
+ acc_bits = 0
132
+ idx = 0
133
+ mask = wide_mask(width)
134
+ count.times do
135
+ while acc_bits < width
136
+ raise Errors.invalid_data("fixed-width u64 underflow") if idx >= bytes.bytesize
137
+
138
+ acc = wide_or(acc, wide_shl(wide_from_u64(bytes.getbyte(idx)), acc_bits))
139
+ idx += 1
140
+ acc_bits += 8
141
+ end
142
+ out << wide_and(acc, mask).lo
143
+ acc = wide_shr(acc, width)
144
+ acc_bits -= width
145
+ end
146
+ raise Errors.invalid_data("fixed-width u64 trailing bytes") unless wide_zero?(acc)
147
+ while idx < bytes.bytesize
148
+ raise Errors.invalid_data("fixed-width u64 trailing bytes") unless bytes.getbyte(idx).zero?
149
+
150
+ idx += 1
151
+ end
152
+ out
153
+ end
154
+
155
+ def wide_zero?(w)
156
+ w.lo.zero? && w.hi.zero?
157
+ end
158
+
159
+ def wide_and(a, m)
160
+ WideU128.new(lo: a.lo & m.lo, hi: a.hi & m.hi)
161
+ end
162
+
163
+ def wide_or(a, b)
164
+ WideU128.new(lo: a.lo | b.lo, hi: a.hi | b.hi)
165
+ end
166
+
167
+ def wide_shl(w, n)
168
+ return w if n.zero?
169
+ return WideU128.new(lo: 0, hi: 0) if n >= 128
170
+
171
+ if n < 64
172
+ hi = ((w.hi << n) | (w.lo >> (64 - n))) & 0xFFFFFFFFFFFFFFFF
173
+ lo = (w.lo << n) & 0xFFFFFFFFFFFFFFFF
174
+ WideU128.new(lo: lo, hi: hi)
175
+ else
176
+ n -= 64
177
+ WideU128.new(lo: 0, hi: (w.lo << n) & 0xFFFFFFFFFFFFFFFF)
178
+ end
179
+ end
180
+
181
+ def wide_shr(w, n)
182
+ return w if n.zero?
183
+ return WideU128.new(lo: 0, hi: 0) if n >= 128
184
+
185
+ if n < 64
186
+ lo = ((w.lo >> n) | (w.hi << (64 - n))) & 0xFFFFFFFFFFFFFFFF
187
+ hi = w.hi >> n
188
+ WideU128.new(lo: lo, hi: hi)
189
+ else
190
+ n -= 64
191
+ WideU128.new(lo: w.hi >> n, hi: 0)
192
+ end
193
+ end
194
+
195
+ def apply_dictionary_references(state, columns)
196
+ columns.each_with_index do |column, i|
197
+ next unless column.values.kind == Model::ElementType::STRING
198
+
199
+ values = column.values.strings
200
+ next if values.length < 16
201
+
202
+ unique = values.uniq
203
+ next if unique.length.to_f / values.length > 0.5
204
+
205
+ codec = column.codec
206
+ next unless codec == Model::VectorCodec::DICTIONARY || codec == Model::VectorCodec::STRING_REF
207
+
208
+ dict_id = state.allocate_dictionary_id
209
+ payload = +""
210
+ keys = unique.sort
211
+ Wire.encode_varuint(keys.length, payload)
212
+ keys.each { |item| Wire.encode_string(item, payload) }
213
+ profile = Session::DictionaryProfile.new(
214
+ version: 1,
215
+ hash: dictionary_payload_hash(payload),
216
+ expires_at: 0,
217
+ fallback: state.options.unknown_reference_policy == Session::UnknownReferencePolicy::STATELESS_RETRY ?
218
+ Session::DictionaryFallback::STATELESS_RETRY : Session::DictionaryFallback::FAIL_FAST
219
+ )
220
+ state.dictionaries[dict_id] = payload
221
+ state.dictionary_profiles[dict_id] = profile
222
+ columns[i] = column.with(dictionary_id: dict_id)
223
+ end
224
+ end
225
+
226
+ def dictionary_payload_hash(payload)
227
+ h = 0xCBF29CE484222325
228
+ payload.each_byte do |b|
229
+ h ^= b
230
+ h = (h * 0x00000100000001B1) & 0xFFFFFFFFFFFFFFFF
231
+ end
232
+ h
233
+ end
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twilic
4
+ module Core
5
+ module Errors
6
+ UNEXPECTED_EOF = :unexpected_eof
7
+ INVALID_KIND = :invalid_kind
8
+ INVALID_TAG = :invalid_tag
9
+ INVALID_DATA = :invalid_data
10
+ UTF8 = :utf8
11
+ UNKNOWN_REFERENCE = :unknown_reference
12
+ STATELESS_RETRY_REQUIRED = :stateless_retry_required
13
+
14
+ class TwilicError < StandardError
15
+ attr_reader :kind, :byte, :msg, :ref_kind, :ref_id
16
+
17
+ def initialize(kind, byte: nil, msg: nil, ref_kind: nil, ref_id: nil)
18
+ @kind = kind
19
+ @byte = byte
20
+ @msg = msg
21
+ @ref_kind = ref_kind
22
+ @ref_id = ref_id
23
+ super(message)
24
+ end
25
+
26
+ def message
27
+ case kind
28
+ when UNEXPECTED_EOF
29
+ "unexpected end of input"
30
+ when INVALID_KIND
31
+ format("invalid message kind: 0x%02x", byte)
32
+ when INVALID_TAG
33
+ format("invalid value tag: 0x%02x", byte)
34
+ when INVALID_DATA
35
+ "invalid data: #{msg}"
36
+ when UTF8
37
+ "utf8 decode error"
38
+ when UNKNOWN_REFERENCE
39
+ "unknown reference: #{ref_kind}=#{ref_id}"
40
+ when STATELESS_RETRY_REQUIRED
41
+ "stateless retry required for reference: #{ref_kind}=#{ref_id}"
42
+ else
43
+ "twilic error"
44
+ end
45
+ end
46
+ end
47
+
48
+ module_function
49
+
50
+ def unexpected_eof
51
+ TwilicError.new(UNEXPECTED_EOF)
52
+ end
53
+
54
+ def invalid_kind(byte)
55
+ TwilicError.new(INVALID_KIND, byte: byte)
56
+ end
57
+
58
+ def invalid_tag(byte)
59
+ TwilicError.new(INVALID_TAG, byte: byte)
60
+ end
61
+
62
+ def invalid_data(msg)
63
+ TwilicError.new(INVALID_DATA, msg: msg)
64
+ end
65
+
66
+ def utf8_error
67
+ TwilicError.new(UTF8)
68
+ end
69
+
70
+ def unknown_reference(kind, id)
71
+ TwilicError.new(UNKNOWN_REFERENCE, ref_kind: kind, ref_id: id)
72
+ end
73
+
74
+ def stateless_retry_required(kind, id)
75
+ TwilicError.new(STATELESS_RETRY_REQUIRED, ref_kind: kind, ref_id: id)
76
+ end
77
+
78
+ def stateless_retry?(err)
79
+ err.is_a?(TwilicError) && err.kind == STATELESS_RETRY_REQUIRED
80
+ end
81
+
82
+ def unknown_reference?(err)
83
+ err.is_a?(TwilicError) && err.kind == UNKNOWN_REFERENCE
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,340 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "twilic/core/model"
4
+ require "twilic/core/protocol"
5
+
6
+ module Twilic
7
+ module Core
8
+ module InteropFixtures
9
+ InteropFrame = Data.define(:stream, :label, :hex, :bytes)
10
+
11
+ module_function
12
+
13
+ def interop_id_name_map(id, name)
14
+ Model.map_value("id" => Model.u64_value(id), "name" => Model.string_value(name))
15
+ end
16
+
17
+ def interop_id_name_role_map(id, name, role)
18
+ Model.map_value(
19
+ "id" => Model.u64_value(id),
20
+ "name" => Model.string_value(name),
21
+ "role" => Model.string_value(role)
22
+ )
23
+ end
24
+
25
+ def interop_make_i64_array(length, start)
26
+ Array.new(length) { |i| Model.i64_value(start + i) }
27
+ end
28
+
29
+ def interop_make_user_rows(names)
30
+ names.each_with_index.map do |name, i|
31
+ Model.map_value("id" => Model.u64_value(i + 1), "name" => Model.string_value(name))
32
+ end
33
+ end
34
+
35
+ def interop_bitpack_control_payload
36
+ Array.new(512) { |i| i.even? ? 0 : 1 }.pack("C*")
37
+ end
38
+
39
+ def interop_huffman_control_payload
40
+ Array.new(512, 7).pack("C*")
41
+ end
42
+
43
+ def interop_fse_control_payload
44
+ Array.new(512) { |i| i % 4 }.pack("C*")
45
+ end
46
+
47
+ def reset_encode_shape_observation(codec, keys)
48
+ key = codec.shape_key(keys)
49
+ codec.state.encode_shape_observations.delete(key)
50
+ end
51
+
52
+ def emit_interop_fixtures(out)
53
+ codec = Protocol::TwilicCodec.new
54
+
55
+ alpha = Model.string_value("alpha")
56
+ emit_interop_value(out, "codec", "scalar_string", codec, alpha)
57
+
58
+ map_two = interop_id_name_map(1, "alice")
59
+ emit_interop_value(out, "codec", "map_two_fields_first", codec, map_two)
60
+ reset_encode_shape_observation(codec, %w[id name])
61
+ emit_interop_value(out, "codec", "map_two_fields_second", codec, map_two)
62
+
63
+ map_three = interop_id_name_role_map(1, "alice", "admin")
64
+ emit_interop_value(out, "codec", "map_three_fields_first", codec, map_three)
65
+ reset_encode_shape_observation(codec, %w[id name role])
66
+ emit_interop_value(out, "codec", "map_three_fields_second", codec, map_three)
67
+
68
+ 8.times do |i|
69
+ dynamic = interop_id_name_map(10 + i, "user-#{i}")
70
+ emit_interop_value(out, "codec", "bulk_map_#{i}", codec, dynamic)
71
+ end
72
+
73
+ scalar = Model.i64_value(42)
74
+ base_snapshot = Model.message(
75
+ kind: Model::MessageKind::BASE_SNAPSHOT,
76
+ base_snapshot: Model::BaseSnapshotMessage.new(
77
+ base_id: 77,
78
+ schema_or_shape_ref: 0,
79
+ payload: Model.message(kind: Model::MessageKind::SCALAR, scalar: scalar)
80
+ )
81
+ )
82
+ emit_interop_message(out, "codec", "base_snapshot", codec, base_snapshot)
83
+
84
+ enc = Protocol::SessionEncoder.new(Session::SessionOptions.default)
85
+ base_array = Model.array_value(interop_make_i64_array(100, 0))
86
+ base_bytes = enc.encode(base_array)
87
+ emit_interop_frame(out, "session", "session_base_array", base_bytes)
88
+
89
+ one_change_arr = interop_make_i64_array(100, 0)
90
+ one_change_arr[0] = Model.i64_value(10_000)
91
+ one_change = Model.array_value(one_change_arr)
92
+ one_patch = enc.encode_patch(one_change)
93
+ emit_interop_frame(out, "session", "session_patch_one_change", one_patch)
94
+
95
+ 4.times do |step|
96
+ iter_arr = interop_make_i64_array(100, 0)
97
+ iter_arr[step] = Model.i64_value(20_000 + step)
98
+ iterative = Model.array_value(iter_arr)
99
+ bytes = enc.encode_patch(iterative)
100
+ emit_interop_frame(out, "session", "session_patch_iter_#{step}", bytes)
101
+ end
102
+
103
+ many_arr = interop_make_i64_array(100, 0)
104
+ 12.times { |idx| many_arr[idx] = Model.i64_value(10_000 + idx) }
105
+ many_change = Model.array_value(many_arr)
106
+ many_patch = enc.encode_patch(many_change)
107
+ emit_interop_frame(out, "session", "session_patch_many_changes", many_patch)
108
+
109
+ rows1 = interop_make_user_rows(%w[a b c d])
110
+ micro_first = enc.encode_micro_batch(rows1)
111
+ emit_interop_frame(out, "session", "session_micro_batch_first", micro_first)
112
+
113
+ rows2 = interop_make_user_rows(%w[aa bb cc dd])
114
+ micro_second = enc.encode_micro_batch(rows2)
115
+ emit_interop_frame(out, "session", "session_micro_batch_second", micro_second)
116
+ end
117
+
118
+ def emit_interop_value(out, stream, label, codec, value)
119
+ bytes = codec.encode_value(value)
120
+ emit_interop_frame(out, stream, label, bytes)
121
+ end
122
+
123
+ def emit_interop_message(out, stream, label, codec, message)
124
+ bytes = codec.encode_message(message)
125
+ emit_interop_frame(out, stream, label, bytes)
126
+ end
127
+
128
+ def emit_interop_frame(out, stream, label, bytes)
129
+ hex = "0123456789abcdef"
130
+ encoded = bytes.each_byte.map { |b| hex[b >> 4] + hex[b & 0x0f] }.join
131
+ frame = "#{stream}|#{label}|#{encoded}\n"
132
+ if out.respond_to?(:<<)
133
+ out << frame
134
+ else
135
+ out.write(frame)
136
+ end
137
+ end
138
+
139
+ def parse_interop_frames(input)
140
+ frames = []
141
+ input.each_line.with_index do |raw_line, line_no|
142
+ line = raw_line.strip
143
+ next if line.empty?
144
+
145
+ begin
146
+ stream, label, hex = parse_interop_frame_line(line)
147
+ bytes = decode_interop_hex(hex)
148
+ frames << InteropFrame.new(stream: stream, label: label, hex: hex, bytes: bytes)
149
+ rescue StandardError => e
150
+ raise "line #{line_no + 1}: #{e.message}"
151
+ end
152
+ end
153
+ raise "no fixture frames found" if frames.empty?
154
+
155
+ frames
156
+ end
157
+
158
+ def parse_interop_frame_line(line)
159
+ first = line.index("|")
160
+ raise "invalid frame" if first.nil? || first <= 0
161
+
162
+ rest = line[(first + 1)..]
163
+ second = rest.index("|")
164
+ raise "invalid frame" if second.nil? || second <= 0
165
+
166
+ [line[0...first], rest[0...second], rest[(second + 1)..]]
167
+ end
168
+
169
+ def decode_interop_hex(hex)
170
+ raise "invalid hex length" unless hex.length.even?
171
+
172
+ hex.chars.each_slice(2).map do |hi, lo|
173
+ (interop_hex_nibble(hi) << 4) | interop_hex_nibble(lo)
174
+ end.pack("C*")
175
+ end
176
+
177
+ def interop_hex_nibble(ch)
178
+ case ch
179
+ when "0".."9" then ch.ord - "0".ord
180
+ when "a".."f" then ch.ord - "a".ord + 10
181
+ when "A".."F" then ch.ord - "A".ord + 10
182
+ else
183
+ raise "invalid hex"
184
+ end
185
+ end
186
+
187
+ def interop_expect_codec_value(label)
188
+ case label
189
+ when "scalar_string"
190
+ [Model.string_value("alpha"), true]
191
+ else
192
+ if label.start_with?("map_two_fields_")
193
+ [interop_id_name_map(1, "alice"), true]
194
+ elsif label.start_with?("map_three_fields_")
195
+ [interop_id_name_role_map(1, "alice", "admin"), true]
196
+ elsif label.start_with?("bulk_map_")
197
+ idx = label.delete_prefix("bulk_map_").to_i
198
+ [interop_id_name_map(10 + idx, "user-#{idx}"), true]
199
+ else
200
+ [nil, false]
201
+ end
202
+ end
203
+ end
204
+
205
+ def interop_expect_codec_value?(label)
206
+ interop_expect_codec_value(label)[1]
207
+ end
208
+
209
+ def interop_expect_control_stream_codec(label)
210
+ case label
211
+ when "control_stream_bitpack" then [Model::ControlStreamCodec::BITPACK, true]
212
+ when "control_stream_huffman" then [Model::ControlStreamCodec::HUFFMAN, true]
213
+ when "control_stream_fse" then [Model::ControlStreamCodec::FSE, true]
214
+ else
215
+ [nil, false]
216
+ end
217
+ end
218
+
219
+ def interop_expect_control_payload(label)
220
+ case label
221
+ when "control_stream_bitpack" then [interop_bitpack_control_payload, true]
222
+ when "control_stream_huffman" then [interop_huffman_control_payload, true]
223
+ when "control_stream_fse" then [interop_fse_control_payload, true]
224
+ else
225
+ [nil, false]
226
+ end
227
+ end
228
+
229
+ def interop_expect_control_payload?(label)
230
+ interop_expect_control_payload(label)[1]
231
+ end
232
+
233
+ def assert_interop_codec_decode(codec, label, frame)
234
+ case label
235
+ when "base_snapshot"
236
+ msg = codec.decode_message(frame)
237
+ raise "expected base snapshot message" unless msg.kind == Model::MessageKind::BASE_SNAPSHOT && msg.base_snapshot
238
+ raise "base_id: got #{msg.base_snapshot.base_id} want 77" unless msg.base_snapshot.base_id == 77
239
+
240
+ payload = msg.base_snapshot.payload
241
+ unless payload.kind == Model::MessageKind::SCALAR &&
242
+ payload.scalar&.kind == Model::ValueKind::I64 &&
243
+ payload.scalar.i64 == 42
244
+ raise "base snapshot payload mismatch"
245
+ end
246
+ return
247
+ end
248
+
249
+ _payload, ok = interop_expect_control_payload(label)
250
+ if ok
251
+ msg = codec.decode_message(frame)
252
+ raise "expected control stream message" unless msg.kind == Model::MessageKind::CONTROL_STREAM && msg.control_stream
253
+ raise "control stream payload empty for #{label}" if msg.control_stream.payload.empty?
254
+
255
+ want_codec, codec_ok = interop_expect_control_stream_codec(label)
256
+ if codec_ok && msg.control_stream.codec != want_codec
257
+ raise "control stream codec mismatch for #{label}"
258
+ end
259
+ return
260
+ end
261
+
262
+ expected, ok = interop_expect_codec_value(label)
263
+ raise "no codec expectation for label #{label.inspect}" unless ok
264
+
265
+ got = codec.decode_value(frame)
266
+ raise "decoded value mismatch for #{label}" unless Model.equal(got, expected)
267
+ end
268
+
269
+ def assert_interop_session_decode(codec, label, frame)
270
+ case label
271
+ when "session_base_array"
272
+ got = codec.decode_value(frame)
273
+ want = Model.array_value(interop_make_i64_array(100, 0))
274
+ raise "session_base_array value mismatch" unless Model.equal(got, want)
275
+ when "session_patch_one_change"
276
+ msg = codec.decode_message(frame)
277
+ want_arr = interop_make_i64_array(100, 0)
278
+ want_arr[0] = Model.i64_value(10_000)
279
+ want = Model.array_value(want_arr)
280
+ case msg.kind
281
+ when Model::MessageKind::STATE_PATCH
282
+ return
283
+ when Model::MessageKind::TYPED_VECTOR
284
+ raise "session_patch_one_change: missing typed vector" unless msg.typed_vector
285
+
286
+ got = Protocol.send(:typed_vector_to_value, msg.typed_vector)
287
+ raise "session_patch_one_change typed vector mismatch" unless Model.equal(got, want)
288
+ when Model::MessageKind::ARRAY
289
+ got = Model.array_value(msg.array)
290
+ raise "session_patch_one_change array mismatch" unless Model.equal(got, want)
291
+ else
292
+ raise "session_patch_one_change: unexpected kind #{msg.kind}"
293
+ end
294
+ when "session_patch_many_changes", "session_micro_batch_first", "session_micro_batch_second"
295
+ msg = codec.decode_message(frame)
296
+ case label
297
+ when "session_patch_many_changes"
298
+ unless [Model::MessageKind::STATE_PATCH, Model::MessageKind::TYPED_VECTOR,
299
+ Model::MessageKind::ARRAY].include?(msg.kind)
300
+ raise "expected patch or array message, got #{msg.kind}"
301
+ end
302
+ when "session_micro_batch_first", "session_micro_batch_second"
303
+ unless msg.kind == Model::MessageKind::TEMPLATE_BATCH && msg.template_batch
304
+ raise "expected template batch message, got #{msg.kind}"
305
+ end
306
+ raise "expected 4 rows, got #{msg.template_batch.count}" unless msg.template_batch.count == 4
307
+ end
308
+ else
309
+ if label.start_with?("session_patch_iter_")
310
+ msg = codec.decode_message(frame)
311
+ unless [Model::MessageKind::STATE_PATCH, Model::MessageKind::TYPED_VECTOR,
312
+ Model::MessageKind::ARRAY].include?(msg.kind)
313
+ raise "#{label}: expected patch or array message, got #{msg.kind}"
314
+ end
315
+ return
316
+ end
317
+ raise "no session expectation for label #{label.inspect}"
318
+ end
319
+ end
320
+
321
+ def replay_codec_state(frames, stop_label)
322
+ iso = Protocol::TwilicCodec.new
323
+ frames.each do |prior|
324
+ next unless prior.stream == "codec"
325
+ break if prior.label == stop_label
326
+
327
+ _payload, ok = interop_expect_control_payload(prior.label)
328
+ if ok || prior.label == "base_snapshot"
329
+ iso.decode_message(prior.bytes)
330
+ next
331
+ end
332
+
333
+ _expected, value_ok = interop_expect_codec_value(prior.label)
334
+ iso.decode_value(prior.bytes) if value_ok
335
+ end
336
+ iso
337
+ end
338
+ end
339
+ end
340
+ end