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.
- checksums.yaml +7 -0
- data/.editorconfig +18 -0
- data/.gitattributes +1 -0
- data/.gitignore +9 -0
- data/.markdownlint.jsonc +22 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +53 -0
- data/LICENSE +21 -0
- data/README.md +119 -0
- data/Rakefile +12 -0
- data/docs/CHANGELOG.md +31 -0
- data/docs/CONTRIBUTING.md +51 -0
- data/docs/SPEC-TEST-TRACEABILITY.md +87 -0
- data/lib/twilic/core/api.rb +30 -0
- data/lib/twilic/core/codec.rb +766 -0
- data/lib/twilic/core/dictionary.rb +236 -0
- data/lib/twilic/core/errors.rb +87 -0
- data/lib/twilic/core/interop_fixtures.rb +340 -0
- data/lib/twilic/core/model.rb +506 -0
- data/lib/twilic/core/protocol.rb +2044 -0
- data/lib/twilic/core/protocol_helpers.rb +512 -0
- data/lib/twilic/core/session.rb +461 -0
- data/lib/twilic/core/v2.rb +387 -0
- data/lib/twilic/core/wire.rb +158 -0
- data/lib/twilic/version.rb +5 -0
- data/lib/twilic.rb +147 -0
- data/package.json +14 -0
- data/pnpm-lock.yaml +723 -0
- data/twilic.gemspec +32 -0
- metadata +118 -0
|
@@ -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
|