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,461 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Twilic
|
|
6
|
+
module Core
|
|
7
|
+
module Session
|
|
8
|
+
class UnknownReferencePolicy
|
|
9
|
+
Entry = Data.define(:name)
|
|
10
|
+
FAIL_FAST = Entry.new(:fail_fast)
|
|
11
|
+
STATELESS_RETRY = Entry.new(:stateless_retry)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class DictionaryFallback
|
|
15
|
+
Entry = Data.define(:name)
|
|
16
|
+
FAIL_FAST = Entry.new(:fail_fast)
|
|
17
|
+
STATELESS_RETRY = Entry.new(:stateless_retry)
|
|
18
|
+
|
|
19
|
+
def self.from_byte(b)
|
|
20
|
+
case b
|
|
21
|
+
when 0 then FAIL_FAST
|
|
22
|
+
when 1 then STATELESS_RETRY
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
DictionaryProfile = Data.define(:version, :hash, :expires_at, :fallback)
|
|
28
|
+
|
|
29
|
+
SessionOptions = Data.define(
|
|
30
|
+
:max_base_snapshots, :enable_state_patch, :enable_template_batch,
|
|
31
|
+
:enable_trained_dictionary, :unknown_reference_policy
|
|
32
|
+
) do
|
|
33
|
+
def self.default
|
|
34
|
+
new(
|
|
35
|
+
max_base_snapshots: 8,
|
|
36
|
+
enable_state_patch: true,
|
|
37
|
+
enable_template_batch: true,
|
|
38
|
+
enable_trained_dictionary: true,
|
|
39
|
+
unknown_reference_policy: UnknownReferencePolicy::FAIL_FAST
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
InternTable = Data.define(:by_value, :by_id) do
|
|
45
|
+
def self.new_table
|
|
46
|
+
new(by_value: {}, by_id: [])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def get_id(value)
|
|
50
|
+
id = by_value[value]
|
|
51
|
+
id ? [id, true] : [0, false]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def get_value(id)
|
|
55
|
+
return ["", false] if id >= by_id.length
|
|
56
|
+
|
|
57
|
+
[by_id[id], true]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def register(value)
|
|
61
|
+
id, ok = get_id(value)
|
|
62
|
+
return id if ok
|
|
63
|
+
|
|
64
|
+
id = by_id.length
|
|
65
|
+
new(by_value: by_value.merge(value => id), by_id: by_id + [value])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def clear
|
|
69
|
+
new(by_value: {}, by_id: [])
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
ShapeTable = Data.define(:by_keys, :by_id, :observations, :next_id) do
|
|
74
|
+
def self.new_table
|
|
75
|
+
new(by_keys: {}, by_id: {}, observations: {}, next_id: 0)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def shape_key(keys)
|
|
79
|
+
keys.join("\0")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def get_id(keys)
|
|
83
|
+
id = by_keys[shape_key(keys)]
|
|
84
|
+
id ? [id, true] : [0, false]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def get_keys(id)
|
|
88
|
+
keys = by_id[id]
|
|
89
|
+
keys ? [keys, true] : [nil, false]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def register(keys)
|
|
93
|
+
sk = shape_key(keys)
|
|
94
|
+
id = by_keys[sk]
|
|
95
|
+
return id if id
|
|
96
|
+
|
|
97
|
+
id = next_id
|
|
98
|
+
keys_copy = keys.dup
|
|
99
|
+
new(
|
|
100
|
+
by_keys: by_keys.merge(sk => id),
|
|
101
|
+
by_id: by_id.merge(id => keys_copy),
|
|
102
|
+
observations: observations,
|
|
103
|
+
next_id: id + 1
|
|
104
|
+
).then { |t| t.by_keys[sk] }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def register_with_id(shape_id, keys)
|
|
108
|
+
sk = shape_key(keys)
|
|
109
|
+
if by_id.key?(shape_id)
|
|
110
|
+
return shape_key(by_id[shape_id]) == sk
|
|
111
|
+
end
|
|
112
|
+
return false if by_keys.key?(sk) && by_keys[sk] != shape_id
|
|
113
|
+
|
|
114
|
+
keys_copy = keys.dup
|
|
115
|
+
new_by_id = by_id.merge(shape_id => keys_copy)
|
|
116
|
+
new_by_keys = by_keys.merge(sk => shape_id)
|
|
117
|
+
new_next = [next_id, shape_id + 1].max
|
|
118
|
+
replace(by_id: new_by_id, by_keys: new_by_keys, next_id: new_next)
|
|
119
|
+
true
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def observe(keys)
|
|
123
|
+
sk = shape_key(keys)
|
|
124
|
+
count = (observations[sk] || 0) + 1
|
|
125
|
+
replace(observations: observations.merge(sk => count))
|
|
126
|
+
count
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def clear
|
|
130
|
+
new(by_keys: {}, by_id: {}, observations: {}, next_id: 0)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
BaseSnapshotEntry = Data.define(:id, :message)
|
|
135
|
+
|
|
136
|
+
SessionState = Data.define(
|
|
137
|
+
:options, :key_table, :string_table, :shape_table, :encode_shape_observations,
|
|
138
|
+
:base_snapshots, :templates, :template_columns, :field_enums, :dictionaries,
|
|
139
|
+
:dictionary_profiles, :schemas, :last_schema_id, :previous_message,
|
|
140
|
+
:previous_message_size, :next_base_id, :next_template_id, :next_dictionary_id
|
|
141
|
+
) do
|
|
142
|
+
def self.new_state
|
|
143
|
+
new(
|
|
144
|
+
options: SessionOptions.default,
|
|
145
|
+
key_table: InternTable.new_table,
|
|
146
|
+
string_table: InternTable.new_table,
|
|
147
|
+
shape_table: ShapeTable.new_table,
|
|
148
|
+
encode_shape_observations: {},
|
|
149
|
+
base_snapshots: [],
|
|
150
|
+
templates: {},
|
|
151
|
+
template_columns: {},
|
|
152
|
+
field_enums: {},
|
|
153
|
+
dictionaries: {},
|
|
154
|
+
dictionary_profiles: {},
|
|
155
|
+
schemas: {},
|
|
156
|
+
last_schema_id: nil,
|
|
157
|
+
previous_message: nil,
|
|
158
|
+
previous_message_size: nil,
|
|
159
|
+
next_base_id: 0,
|
|
160
|
+
next_template_id: 0,
|
|
161
|
+
next_dictionary_id: 0
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def self.with_options(options)
|
|
166
|
+
new_state.with(options: options)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def register_base_snapshot(base_id, message)
|
|
170
|
+
filtered = base_snapshots.reject { |e| e.id == base_id }
|
|
171
|
+
filtered << BaseSnapshotEntry.new(id: base_id, message: message.clone_message)
|
|
172
|
+
while filtered.length > options.max_base_snapshots
|
|
173
|
+
filtered.shift
|
|
174
|
+
end
|
|
175
|
+
with(base_snapshots: filtered)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def allocate_base_id
|
|
179
|
+
id = next_base_id
|
|
180
|
+
with(next_base_id: next_base_id + 1).next_base_id == id ? id : id
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def allocate_template_id
|
|
184
|
+
id = next_template_id
|
|
185
|
+
with(next_template_id: next_template_id + 1)
|
|
186
|
+
id
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def allocate_dictionary_id
|
|
190
|
+
id = next_dictionary_id
|
|
191
|
+
with(next_dictionary_id: next_dictionary_id + 1)
|
|
192
|
+
id
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def get_base_snapshot(base_id)
|
|
196
|
+
entry = base_snapshots.find { |e| e.id == base_id }
|
|
197
|
+
return [nil, false] unless entry
|
|
198
|
+
|
|
199
|
+
[entry.message.clone_message, true]
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def reset_tables
|
|
203
|
+
with(
|
|
204
|
+
key_table: InternTable.new_table,
|
|
205
|
+
string_table: InternTable.new_table,
|
|
206
|
+
shape_table: ShapeTable.new_table,
|
|
207
|
+
encode_shape_observations: {},
|
|
208
|
+
field_enums: {}
|
|
209
|
+
)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def reset_state
|
|
213
|
+
reset_tables.with(
|
|
214
|
+
base_snapshots: [],
|
|
215
|
+
templates: {},
|
|
216
|
+
template_columns: {},
|
|
217
|
+
dictionaries: {},
|
|
218
|
+
dictionary_profiles: {},
|
|
219
|
+
schemas: {},
|
|
220
|
+
last_schema_id: nil,
|
|
221
|
+
previous_message: nil,
|
|
222
|
+
previous_message_size: nil,
|
|
223
|
+
next_base_id: 0,
|
|
224
|
+
next_template_id: 0,
|
|
225
|
+
next_dictionary_id: 0
|
|
226
|
+
)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Mutable wrapper for session state used during encode/decode
|
|
231
|
+
class MutableSessionState
|
|
232
|
+
attr_accessor :options, :key_table, :string_table, :shape_table,
|
|
233
|
+
:encode_shape_observations, :base_snapshots, :templates,
|
|
234
|
+
:template_columns, :field_enums, :dictionaries,
|
|
235
|
+
:dictionary_profiles, :schemas, :last_schema_id,
|
|
236
|
+
:previous_message, :previous_message_size,
|
|
237
|
+
:next_base_id, :next_template_id, :next_dictionary_id
|
|
238
|
+
|
|
239
|
+
def initialize(options = SessionOptions.default)
|
|
240
|
+
@options = options
|
|
241
|
+
@key_table = MutableInternTable.new
|
|
242
|
+
@string_table = MutableInternTable.new
|
|
243
|
+
@shape_table = MutableShapeTable.new
|
|
244
|
+
@encode_shape_observations = {}
|
|
245
|
+
@base_snapshots = []
|
|
246
|
+
@templates = {}
|
|
247
|
+
@template_columns = {}
|
|
248
|
+
@field_enums = {}
|
|
249
|
+
@dictionaries = {}
|
|
250
|
+
@dictionary_profiles = {}
|
|
251
|
+
@schemas = {}
|
|
252
|
+
@last_schema_id = nil
|
|
253
|
+
@previous_message = nil
|
|
254
|
+
@previous_message_size = nil
|
|
255
|
+
@next_base_id = 0
|
|
256
|
+
@next_template_id = 0
|
|
257
|
+
@next_dictionary_id = 0
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def shape_key(keys)
|
|
261
|
+
keys.join("\0")
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def register_base_snapshot(base_id, message)
|
|
265
|
+
@base_snapshots.reject! { |e| e.id == base_id }
|
|
266
|
+
@base_snapshots << BaseSnapshotEntry.new(id: base_id, message: message.clone_message)
|
|
267
|
+
while @base_snapshots.length > @options.max_base_snapshots
|
|
268
|
+
@base_snapshots.shift
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def allocate_base_id
|
|
273
|
+
id = @next_base_id
|
|
274
|
+
@next_base_id += 1
|
|
275
|
+
id
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def allocate_template_id
|
|
279
|
+
id = @next_template_id
|
|
280
|
+
@next_template_id += 1
|
|
281
|
+
id
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def allocate_dictionary_id
|
|
285
|
+
id = @next_dictionary_id
|
|
286
|
+
@next_dictionary_id += 1
|
|
287
|
+
id
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def get_base_snapshot(base_id)
|
|
291
|
+
entry = @base_snapshots.find { |e| e.id == base_id }
|
|
292
|
+
return [nil, false] unless entry
|
|
293
|
+
|
|
294
|
+
[entry.message.clone_message, true]
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def reset_tables
|
|
298
|
+
@key_table = MutableInternTable.new
|
|
299
|
+
@string_table = MutableInternTable.new
|
|
300
|
+
@shape_table = MutableShapeTable.new
|
|
301
|
+
@encode_shape_observations = {}
|
|
302
|
+
@field_enums = {}
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def reset_state
|
|
306
|
+
reset_tables
|
|
307
|
+
@base_snapshots = []
|
|
308
|
+
@templates = {}
|
|
309
|
+
@template_columns = {}
|
|
310
|
+
@dictionaries = {}
|
|
311
|
+
@dictionary_profiles = {}
|
|
312
|
+
@schemas = {}
|
|
313
|
+
@last_schema_id = nil
|
|
314
|
+
@previous_message = nil
|
|
315
|
+
@previous_message_size = nil
|
|
316
|
+
@next_base_id = 0
|
|
317
|
+
@next_template_id = 0
|
|
318
|
+
@next_dictionary_id = 0
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
module InternTableHelpers
|
|
323
|
+
module_function
|
|
324
|
+
|
|
325
|
+
def get_id(table, value)
|
|
326
|
+
id = table.by_value[value]
|
|
327
|
+
id ? [id, true] : [0, false]
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def get_value(table, id)
|
|
331
|
+
return ["", false] if id >= table.by_id.length
|
|
332
|
+
|
|
333
|
+
[table.by_id[id], true]
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def register(table, value)
|
|
337
|
+
id, ok = get_id(table, value)
|
|
338
|
+
return [table, id] if ok
|
|
339
|
+
|
|
340
|
+
id = table.by_id.length
|
|
341
|
+
new_table = InternTable.new(
|
|
342
|
+
by_value: table.by_value.merge(value => id),
|
|
343
|
+
by_id: table.by_id + [value]
|
|
344
|
+
)
|
|
345
|
+
[new_table, id]
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def register_mut(table, value)
|
|
349
|
+
id = table.by_value[value]
|
|
350
|
+
return id if id
|
|
351
|
+
|
|
352
|
+
id = table.by_id.length
|
|
353
|
+
table.by_value[value] = id
|
|
354
|
+
table.by_id << value
|
|
355
|
+
id
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
class MutableInternTable
|
|
360
|
+
attr_reader :by_value, :by_id
|
|
361
|
+
|
|
362
|
+
def initialize
|
|
363
|
+
@by_value = {}
|
|
364
|
+
@by_id = []
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def get_id(value)
|
|
368
|
+
id = @by_value[value]
|
|
369
|
+
id ? [id, true] : [0, false]
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def get_value(id)
|
|
373
|
+
return ["", false] if id >= @by_id.length
|
|
374
|
+
|
|
375
|
+
[@by_id[id], true]
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def register(value)
|
|
379
|
+
id = @by_value[value]
|
|
380
|
+
return id if id
|
|
381
|
+
|
|
382
|
+
id = @by_id.length
|
|
383
|
+
@by_value[value] = id
|
|
384
|
+
@by_id << value
|
|
385
|
+
id
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def clear
|
|
389
|
+
@by_value = {}
|
|
390
|
+
@by_id = []
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
class MutableShapeTable
|
|
395
|
+
attr_reader :by_keys, :by_id, :observations, :next_id
|
|
396
|
+
|
|
397
|
+
def initialize
|
|
398
|
+
@by_keys = {}
|
|
399
|
+
@by_id = {}
|
|
400
|
+
@observations = {}
|
|
401
|
+
@next_id = 0
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def shape_key(keys)
|
|
405
|
+
keys.join("\0")
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def get_id(keys)
|
|
409
|
+
id = @by_keys[shape_key(keys)]
|
|
410
|
+
id ? [id, true] : [0, false]
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def get_keys(id)
|
|
414
|
+
keys = @by_id[id]
|
|
415
|
+
keys ? [keys.dup, true] : [nil, false]
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def register(keys)
|
|
419
|
+
sk = shape_key(keys)
|
|
420
|
+
id = @by_keys[sk]
|
|
421
|
+
return id if id
|
|
422
|
+
|
|
423
|
+
id = @next_id
|
|
424
|
+
@next_id += 1
|
|
425
|
+
@by_id[id] = keys.dup
|
|
426
|
+
@by_keys[sk] = id
|
|
427
|
+
id
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def register_with_id(shape_id, keys)
|
|
431
|
+
sk = shape_key(keys)
|
|
432
|
+
if @by_id.key?(shape_id)
|
|
433
|
+
return shape_key(@by_id[shape_id]) == sk
|
|
434
|
+
end
|
|
435
|
+
return false if @by_keys.key?(sk) && @by_keys[sk] != shape_id
|
|
436
|
+
|
|
437
|
+
@by_id[shape_id] = keys.dup
|
|
438
|
+
@by_keys[sk] = shape_id
|
|
439
|
+
@next_id = [@next_id, shape_id + 1].max
|
|
440
|
+
true
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def observe(keys)
|
|
444
|
+
sk = shape_key(keys)
|
|
445
|
+
@observations[sk] = (@observations[sk] || 0) + 1
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def observation_count(keys)
|
|
449
|
+
@observations[shape_key(keys)] || 0
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def clear
|
|
453
|
+
@by_keys = {}
|
|
454
|
+
@by_id = {}
|
|
455
|
+
@observations = {}
|
|
456
|
+
@next_id = 0
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
end
|