@axpecter/lync 2.3.1 → 2.3.2
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.
- package/README.md +29 -19
- package/package.json +1 -1
- package/src/Types.luau +12 -6
- package/src/api/Packet.luau +95 -35
- package/src/codec/Base.luau +7 -0
- package/src/codec/composite/Array.luau +274 -15
- package/src/codec/composite/Map.luau +328 -39
- package/src/codec/composite/Optional.luau +4 -0
- package/src/codec/composite/Shared.luau +60 -81
- package/src/codec/composite/Struct.luau +13 -3
- package/src/codec/composite/Tagged.luau +8 -0
- package/src/codec/composite/Tuple.luau +12 -1
- package/src/codec/datatype/Buffer.luau +1 -3
- package/src/codec/datatype/CFrame.luau +6 -106
- package/src/codec/meta/DeltaScalar.luau +390 -0
- package/src/codec/primitive/Varint.luau +13 -7
- package/src/codec/primitive/Zint.luau +99 -0
- package/src/index.d.ts +46 -0
- package/src/init.luau +7 -0
- package/src/internal/Channel.luau +60 -25
- package/src/transport/Reader.luau +16 -5
- package/src/util/Buffer.luau +2 -4
- package/src/util/Quantize.luau +27 -3
- package/src/util/Quat.luau +124 -0
|
@@ -9,6 +9,13 @@ local Shared = require(script.Parent.Parent.composite.Shared)
|
|
|
9
9
|
local Types = require(script.Parent.Parent.Parent.Types)
|
|
10
10
|
local Varint = require(script.Parent.Parent.primitive.Varint)
|
|
11
11
|
|
|
12
|
+
-- Constants --------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
-- deltaMap wire flags. Distinct from Shared's struct/array flag spaces.
|
|
15
|
+
local DM_UNCHANGED = 0
|
|
16
|
+
local DM_FULL = 1
|
|
17
|
+
local DM_DIFF = 2
|
|
18
|
+
|
|
12
19
|
-- State ------------------------------------------------------------------
|
|
13
20
|
|
|
14
21
|
--[[
|
|
@@ -21,8 +28,22 @@ local _keyDepth = 0
|
|
|
21
28
|
|
|
22
29
|
-- Private ----------------------------------------------------------------
|
|
23
30
|
|
|
24
|
-
local
|
|
31
|
+
local ensure = Base.ensure
|
|
25
32
|
local allocDeltaId = Shared.allocDeltaId
|
|
33
|
+
local acquireScratch = Shared.acquireScratch
|
|
34
|
+
local releaseScratch = Shared.releaseScratch
|
|
35
|
+
local rangeEqual = Shared.rangeEqual
|
|
36
|
+
local copyToBuf = Shared.copyToBuf
|
|
37
|
+
local containsDelta = Shared.containsDelta
|
|
38
|
+
local enforceMaxCount = Shared.enforceMaxCount
|
|
39
|
+
local writeu8 = buffer.writeu8
|
|
40
|
+
local readu8 = buffer.readu8
|
|
41
|
+
local bufCopy = buffer.copy
|
|
42
|
+
local bufLen = buffer.len
|
|
43
|
+
local varintRead = Varint.read
|
|
44
|
+
local varintWrite = Varint.write
|
|
45
|
+
|
|
46
|
+
type DeltaMapEntry = { kBuf: buffer, kLen: number, vBuf: buffer, vLen: number }
|
|
26
47
|
|
|
27
48
|
local function acquireKeys(): { any }
|
|
28
49
|
_keyDepth += 1
|
|
@@ -44,6 +65,34 @@ local function tostringLess(a: any, b: any): boolean
|
|
|
44
65
|
return tostring(a) < tostring(b)
|
|
45
66
|
end
|
|
46
67
|
|
|
68
|
+
local function sortKeys(keys: { any }, canSortDirect: boolean): ()
|
|
69
|
+
if canSortDirect then
|
|
70
|
+
table.sort(keys :: { any })
|
|
71
|
+
else
|
|
72
|
+
table.sort(keys, tostringLess)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
--[[
|
|
77
|
+
Acquire a scratch keys array, populate it from `value`, nil any leftover
|
|
78
|
+
tail entries from a prior reuse, then sort. Returns the keys array and
|
|
79
|
+
the populated count. Caller MUST releaseKeys() after consuming the keys.
|
|
80
|
+
]]
|
|
81
|
+
local function collectAndSortKeys(value: { [any]: any }, canSortDirect: boolean): ({ any }, number)
|
|
82
|
+
local keys = acquireKeys()
|
|
83
|
+
local count = 0
|
|
84
|
+
for k in value do
|
|
85
|
+
count += 1
|
|
86
|
+
keys[count] = k
|
|
87
|
+
end
|
|
88
|
+
local prevTail = #keys
|
|
89
|
+
for i = count + 1, prevTail do
|
|
90
|
+
keys[i] = nil
|
|
91
|
+
end
|
|
92
|
+
sortKeys(keys, canSortDirect)
|
|
93
|
+
return keys, count
|
|
94
|
+
end
|
|
95
|
+
|
|
47
96
|
-- Public -----------------------------------------------------------------
|
|
48
97
|
|
|
49
98
|
local Map = {}
|
|
@@ -56,6 +105,7 @@ function Map.map(
|
|
|
56
105
|
local fixedKey = keyCodec._size
|
|
57
106
|
local fixedValue = valueCodec._size
|
|
58
107
|
local hasUnknown = keyCodec._hasUnknown or valueCodec._hasUnknown
|
|
108
|
+
local hasDelta = containsDelta(keyCodec) or containsDelta(valueCodec)
|
|
59
109
|
|
|
60
110
|
--[[
|
|
61
111
|
Numbers and strings sort by `<` directly; everything else (Instance,
|
|
@@ -82,61 +132,35 @@ function Map.map(
|
|
|
82
132
|
Log.error(`expected table, got {typeof(value)}`)
|
|
83
133
|
end
|
|
84
134
|
|
|
85
|
-
local keys =
|
|
86
|
-
local count = 0
|
|
87
|
-
for k in value do
|
|
88
|
-
count += 1
|
|
89
|
-
keys[count] = k
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
-- Drop tail entries from the prior reuse so table.sort sees only `count` items.
|
|
93
|
-
local prevTail = #keys
|
|
94
|
-
for i = count + 1, prevTail do
|
|
95
|
-
keys[i] = nil
|
|
96
|
-
end
|
|
135
|
+
local keys, count = collectAndSortKeys(value, canSortDirect)
|
|
97
136
|
|
|
98
|
-
|
|
137
|
+
varintWrite(ch, count)
|
|
99
138
|
if count == 0 then
|
|
100
139
|
releaseKeys()
|
|
101
140
|
return
|
|
102
141
|
end
|
|
103
142
|
|
|
104
|
-
if canSortDirect then
|
|
105
|
-
table.sort(keys :: { any })
|
|
106
|
-
else
|
|
107
|
-
table.sort(keys, tostringLess)
|
|
108
|
-
end
|
|
109
|
-
|
|
110
143
|
-- Single alloc when both halves are fixed-size.
|
|
111
144
|
if fixedKey and fixedValue then
|
|
112
|
-
|
|
113
|
-
local cursor = ch.cursor
|
|
114
|
-
if cursor + count * entrySize > ch.size then
|
|
115
|
-
alloc(ch, count * entrySize)
|
|
116
|
-
end
|
|
145
|
+
ensure(ch, count * (fixedKey + fixedValue))
|
|
117
146
|
end
|
|
118
147
|
|
|
119
148
|
for i = 1, count do
|
|
120
149
|
local k = keys[i]
|
|
121
150
|
keyCodec.write(ch, k)
|
|
122
151
|
valueCodec.write(ch, value[k])
|
|
123
|
-
keys[i] = nil
|
|
124
152
|
end
|
|
125
153
|
releaseKeys()
|
|
126
154
|
end,
|
|
127
155
|
|
|
128
156
|
read = function(src: buffer, pos: number, refs: { Instance }?): ({ [any]: any }, number)
|
|
129
|
-
local count, lenBytes =
|
|
130
|
-
|
|
131
|
-
Log.error(`count {count} exceeds max {maxCount}`)
|
|
132
|
-
end
|
|
157
|
+
local count, lenBytes = varintRead(src, pos)
|
|
158
|
+
enforceMaxCount(count, maxCount)
|
|
133
159
|
|
|
134
160
|
-- Fixed-size: reject corrupt counts before per-entry decode.
|
|
135
161
|
if fixedKey and fixedValue and count > 0 then
|
|
136
|
-
local
|
|
137
|
-
|
|
138
|
-
local payloadBytes = count * entrySize
|
|
139
|
-
if payloadBytes > buffer.len(src) - dataStart then
|
|
162
|
+
local payloadBytes = count * (fixedKey + fixedValue)
|
|
163
|
+
if payloadBytes > bufLen(src) - (pos + lenBytes) then
|
|
140
164
|
Log.error(`payload {payloadBytes}B exceeds remaining buffer`)
|
|
141
165
|
end
|
|
142
166
|
end
|
|
@@ -162,19 +186,284 @@ function Map.map(
|
|
|
162
186
|
if hasUnknown then
|
|
163
187
|
codec._hasUnknown = true
|
|
164
188
|
end
|
|
189
|
+
if hasDelta then
|
|
190
|
+
codec._hasDelta = true
|
|
191
|
+
end
|
|
165
192
|
return table.freeze(codec)
|
|
166
193
|
end
|
|
167
194
|
|
|
195
|
+
--[[
|
|
196
|
+
Per-key deltaMap. Wire:
|
|
197
|
+
[0] UNCHANGED (1 byte)
|
|
198
|
+
[1] FULL <count varint> <(k,v) pairs ...>
|
|
199
|
+
[2] DIFF <removeCount varint> <removed key bytes ...>
|
|
200
|
+
<changeCount varint> <(k,v) pair bytes ...>
|
|
201
|
+
|
|
202
|
+
Cache: per-key encoded value bytes for byte-equality diffing plus per-key
|
|
203
|
+
encoded key bytes so removed-key emission doesn't re-encode the key codec
|
|
204
|
+
for keys that no longer live in the map.
|
|
205
|
+
]]
|
|
168
206
|
function Map.deltaMap(
|
|
169
207
|
keyCodec: Types.InternalCodec<any>,
|
|
170
208
|
valueCodec: Types.InternalCodec<any>,
|
|
171
209
|
maxCount: number?
|
|
172
210
|
): Types.InternalCodec<any>
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
)
|
|
211
|
+
-- Inner delta would chain-diff across pairs within a frame, not across frames.
|
|
212
|
+
if containsDelta(keyCodec) then
|
|
213
|
+
Log.error("deltaMap key codec cannot contain delta state")
|
|
214
|
+
end
|
|
215
|
+
if containsDelta(valueCodec) then
|
|
216
|
+
Log.error("deltaMap value codec cannot contain delta state")
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
local deltaId = allocDeltaId()
|
|
220
|
+
local typeCheck = keyCodec._typeCheck
|
|
221
|
+
local canSortDirect = typeCheck == "number" or typeCheck == "string"
|
|
222
|
+
local inner = Map.map(keyCodec, valueCodec, maxCount)
|
|
223
|
+
|
|
224
|
+
--[[
|
|
225
|
+
Encode each (key, value) pair into `scratch` and capture per-pair byte
|
|
226
|
+
ranges. Returns the sorted-keys list plus four parallel arrays of
|
|
227
|
+
offset/length so the diff path can compare and emit individual entries
|
|
228
|
+
without re-encoding.
|
|
229
|
+
]]
|
|
230
|
+
local function encodePerKey(
|
|
231
|
+
scratch: Types.ChannelState,
|
|
232
|
+
value: { [any]: any }
|
|
233
|
+
): ({ any }, { number }, { number }, { number }, { number })
|
|
234
|
+
local keys, count = collectAndSortKeys(value, canSortDirect)
|
|
235
|
+
local kOff = table.create(count)
|
|
236
|
+
local kLen = table.create(count)
|
|
237
|
+
local vOff = table.create(count)
|
|
238
|
+
local vLen = table.create(count)
|
|
239
|
+
|
|
240
|
+
for i = 1, count do
|
|
241
|
+
local k = keys[i]
|
|
242
|
+
local before = scratch.cursor
|
|
243
|
+
keyCodec.write(scratch, k)
|
|
244
|
+
local mid = scratch.cursor
|
|
245
|
+
kOff[i] = before
|
|
246
|
+
kLen[i] = mid - before
|
|
247
|
+
valueCodec.write(scratch, value[k])
|
|
248
|
+
vOff[i] = mid
|
|
249
|
+
vLen[i] = scratch.cursor - mid
|
|
250
|
+
end
|
|
251
|
+
return keys, kOff, kLen, vOff, vLen
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
return table.freeze({
|
|
255
|
+
_isDelta = true,
|
|
256
|
+
_isMap = true,
|
|
257
|
+
_keyCodec = keyCodec,
|
|
258
|
+
_valueCodec = valueCodec,
|
|
259
|
+
_maxCount = maxCount,
|
|
260
|
+
|
|
261
|
+
write = function(ch: Types.ChannelState, value: { [any]: any }): ()
|
|
262
|
+
if typeof(value) ~= "table" then
|
|
263
|
+
Log.error(`expected table, got {typeof(value)}`)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
local cache = ch.deltas[deltaId] :: any
|
|
267
|
+
local scratch = acquireScratch()
|
|
268
|
+
local keys, kOff, kLen, vOff, vLen = encodePerKey(scratch, value)
|
|
269
|
+
local count = #keys
|
|
270
|
+
local scratchBuf = scratch.buff
|
|
271
|
+
|
|
272
|
+
-- First frame: emit FULL. Per-key scratch bytes ARE the inner map payload.
|
|
273
|
+
if not cache then
|
|
274
|
+
ensure(ch, 1)
|
|
275
|
+
writeu8(ch.buff, ch.cursor, DM_FULL)
|
|
276
|
+
ch.cursor += 1
|
|
277
|
+
varintWrite(ch, count)
|
|
278
|
+
local payloadLen = scratch.cursor
|
|
279
|
+
ensure(ch, payloadLen)
|
|
280
|
+
bufCopy(ch.buff, ch.cursor, scratchBuf, 0, payloadLen)
|
|
281
|
+
ch.cursor += payloadLen
|
|
282
|
+
|
|
283
|
+
local perKey: { [any]: DeltaMapEntry } = {}
|
|
284
|
+
for i = 1, count do
|
|
285
|
+
local kbLen = kLen[i]
|
|
286
|
+
local vbLen = vLen[i]
|
|
287
|
+
perKey[keys[i]] = {
|
|
288
|
+
kBuf = copyToBuf(nil, scratchBuf, kOff[i], kbLen),
|
|
289
|
+
kLen = kbLen,
|
|
290
|
+
vBuf = copyToBuf(nil, scratchBuf, vOff[i], vbLen),
|
|
291
|
+
vLen = vbLen,
|
|
292
|
+
}
|
|
293
|
+
end
|
|
294
|
+
ch.deltas[deltaId] = { perKey = perKey } :: any
|
|
295
|
+
releaseKeys()
|
|
296
|
+
releaseScratch()
|
|
297
|
+
return
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
local cachedPerKey = cache.perKey :: { [any]: DeltaMapEntry }
|
|
301
|
+
|
|
302
|
+
local seen: { [any]: boolean } = {}
|
|
303
|
+
local changed: { number } = {}
|
|
304
|
+
local changedCount = 0
|
|
305
|
+
for i = 1, count do
|
|
306
|
+
local k = keys[i]
|
|
307
|
+
seen[k] = true
|
|
308
|
+
local entry = cachedPerKey[k]
|
|
309
|
+
local newLen = vLen[i]
|
|
310
|
+
if
|
|
311
|
+
not entry
|
|
312
|
+
or newLen ~= entry.vLen
|
|
313
|
+
or not rangeEqual(scratchBuf, vOff[i], entry.vBuf, 0, newLen)
|
|
314
|
+
then
|
|
315
|
+
changedCount += 1
|
|
316
|
+
changed[changedCount] = i
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
local removed: { any } = {}
|
|
321
|
+
local removedCount = 0
|
|
322
|
+
for k in cachedPerKey do
|
|
323
|
+
if not seen[k] then
|
|
324
|
+
removedCount += 1
|
|
325
|
+
removed[removedCount] = k
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
if removedCount == 0 and changedCount == 0 then
|
|
330
|
+
ensure(ch, 1)
|
|
331
|
+
writeu8(ch.buff, ch.cursor, DM_UNCHANGED)
|
|
332
|
+
ch.cursor += 1
|
|
333
|
+
releaseKeys()
|
|
334
|
+
releaseScratch()
|
|
335
|
+
return
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
-- Sort removed keys so the XOR baseline can collapse identical drops.
|
|
339
|
+
sortKeys(removed, canSortDirect)
|
|
340
|
+
|
|
341
|
+
ensure(ch, 1)
|
|
342
|
+
writeu8(ch.buff, ch.cursor, DM_DIFF)
|
|
343
|
+
ch.cursor += 1
|
|
344
|
+
|
|
345
|
+
varintWrite(ch, removedCount)
|
|
346
|
+
for i = 1, removedCount do
|
|
347
|
+
local entry = cachedPerKey[removed[i]]
|
|
348
|
+
local n = entry.kLen
|
|
349
|
+
ensure(ch, n)
|
|
350
|
+
bufCopy(ch.buff, ch.cursor, entry.kBuf, 0, n)
|
|
351
|
+
ch.cursor += n
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
varintWrite(ch, changedCount)
|
|
355
|
+
for i = 1, changedCount do
|
|
356
|
+
local idx = changed[i]
|
|
357
|
+
local kbLen = kLen[idx]
|
|
358
|
+
local vbLen = vLen[idx]
|
|
359
|
+
ensure(ch, kbLen + vbLen)
|
|
360
|
+
bufCopy(ch.buff, ch.cursor, scratchBuf, kOff[idx], kbLen)
|
|
361
|
+
bufCopy(ch.buff, ch.cursor + kbLen, scratchBuf, vOff[idx], vbLen)
|
|
362
|
+
ch.cursor += kbLen + vbLen
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
-- Update cache: drop removed, refresh changed, leave untouched in place.
|
|
366
|
+
for i = 1, removedCount do
|
|
367
|
+
cachedPerKey[removed[i]] = nil
|
|
368
|
+
end
|
|
369
|
+
for i = 1, changedCount do
|
|
370
|
+
local idx = changed[i]
|
|
371
|
+
local k = keys[idx]
|
|
372
|
+
local kbLen = kLen[idx]
|
|
373
|
+
local vbLen = vLen[idx]
|
|
374
|
+
local entry = cachedPerKey[k]
|
|
375
|
+
if entry then
|
|
376
|
+
entry.kBuf = copyToBuf(entry.kBuf, scratchBuf, kOff[idx], kbLen)
|
|
377
|
+
entry.kLen = kbLen
|
|
378
|
+
entry.vBuf = copyToBuf(entry.vBuf, scratchBuf, vOff[idx], vbLen)
|
|
379
|
+
entry.vLen = vbLen
|
|
380
|
+
else
|
|
381
|
+
cachedPerKey[k] = {
|
|
382
|
+
kBuf = copyToBuf(nil, scratchBuf, kOff[idx], kbLen),
|
|
383
|
+
kLen = kbLen,
|
|
384
|
+
vBuf = copyToBuf(nil, scratchBuf, vOff[idx], vbLen),
|
|
385
|
+
vLen = vbLen,
|
|
386
|
+
}
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
releaseKeys()
|
|
391
|
+
releaseScratch()
|
|
392
|
+
end,
|
|
393
|
+
|
|
394
|
+
read = function(src: buffer, pos: number, refs: { Instance }?): ({ [any]: any }, number)
|
|
395
|
+
if pos >= bufLen(src) then
|
|
396
|
+
Log.error("truncated deltaMap header")
|
|
397
|
+
end
|
|
398
|
+
local flag = readu8(src, pos)
|
|
399
|
+
|
|
400
|
+
if flag == DM_UNCHANGED then
|
|
401
|
+
local cached = Baseline.getCache(deltaId)
|
|
402
|
+
if cached == nil then
|
|
403
|
+
Log.error("UNCHANGED flag before any FULL frame; baseline missing")
|
|
404
|
+
end
|
|
405
|
+
return cached, 1
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
if flag == DM_FULL then
|
|
409
|
+
local value, consumed = inner.read(src, pos + 1, refs)
|
|
410
|
+
Baseline.setCache(deltaId, value)
|
|
411
|
+
return value, 1 + consumed
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
if flag == DM_DIFF then
|
|
415
|
+
local cached = Baseline.getCache(deltaId)
|
|
416
|
+
if cached == nil then
|
|
417
|
+
Log.error("deltaMap DIFF before any FULL frame; baseline missing")
|
|
418
|
+
end
|
|
419
|
+
local result: { [any]: any } = table.clone(cached)
|
|
420
|
+
local total = 1
|
|
421
|
+
|
|
422
|
+
local removeCount, rcLen = varintRead(src, pos + total)
|
|
423
|
+
if rcLen == 0 then
|
|
424
|
+
Log.error("truncated deltaMap remove count")
|
|
425
|
+
end
|
|
426
|
+
-- Both counts bounded by maxCount; otherwise a malicious DIFF
|
|
427
|
+
-- could spin the loop arbitrarily long before erroring on
|
|
428
|
+
-- buffer overrun.
|
|
429
|
+
enforceMaxCount(removeCount, maxCount, "removeCount")
|
|
430
|
+
total += rcLen
|
|
431
|
+
for _ = 1, removeCount do
|
|
432
|
+
local k, kc = keyCodec.read(src, pos + total, refs)
|
|
433
|
+
if kc == 0 then
|
|
434
|
+
Log.error("truncated deltaMap removed key")
|
|
435
|
+
end
|
|
436
|
+
total += kc
|
|
437
|
+
result[k] = nil
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
local changeCount, ccLen = varintRead(src, pos + total)
|
|
441
|
+
if ccLen == 0 then
|
|
442
|
+
Log.error("truncated deltaMap change count")
|
|
443
|
+
end
|
|
444
|
+
enforceMaxCount(changeCount, maxCount, "changeCount")
|
|
445
|
+
total += ccLen
|
|
446
|
+
for _ = 1, changeCount do
|
|
447
|
+
local k, kc = keyCodec.read(src, pos + total, refs)
|
|
448
|
+
if kc == 0 then
|
|
449
|
+
Log.error("truncated deltaMap changed key")
|
|
450
|
+
end
|
|
451
|
+
total += kc
|
|
452
|
+
local v, vc = valueCodec.read(src, pos + total, refs)
|
|
453
|
+
if vc == 0 then
|
|
454
|
+
Log.error("truncated deltaMap changed value")
|
|
455
|
+
end
|
|
456
|
+
total += vc
|
|
457
|
+
result[k] = v
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
Baseline.setCache(deltaId, result)
|
|
461
|
+
return result, total
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
Log.error(`invalid deltaMap flag {flag}`)
|
|
465
|
+
end,
|
|
466
|
+
}) :: Types.InternalCodec<any>
|
|
178
467
|
end
|
|
179
468
|
|
|
180
469
|
return table.freeze(Map)
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
local Channel = require(script.Parent.Parent.Parent.internal.Channel)
|
|
6
6
|
local Log = require(script.Parent.Parent.Parent.util.Log)
|
|
7
|
+
local Shared = require(script.Parent.Parent.composite.Shared)
|
|
7
8
|
local Types = require(script.Parent.Parent.Parent.Types)
|
|
8
9
|
|
|
9
10
|
-- Private ----------------------------------------------------------------
|
|
@@ -44,6 +45,9 @@ function Optional.optional(inner: Types.InternalCodec<any>): Types.InternalCodec
|
|
|
44
45
|
if inner._hasUnknown then
|
|
45
46
|
codec._hasUnknown = true
|
|
46
47
|
end
|
|
48
|
+
if Shared.containsDelta(inner) then
|
|
49
|
+
codec._hasDelta = true
|
|
50
|
+
end
|
|
47
51
|
return table.freeze(codec)
|
|
48
52
|
end
|
|
49
53
|
|
|
@@ -11,14 +11,14 @@ local Types = require(script.Parent.Parent.Parent.Types)
|
|
|
11
11
|
-- Constants --------------------------------------------------------------
|
|
12
12
|
|
|
13
13
|
--[[
|
|
14
|
-
Delta flag
|
|
15
|
-
0 UNCHANGED
|
|
16
|
-
1
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
Delta flag base shared by deltaStruct's per-segment scheme:
|
|
15
|
+
0 UNCHANGED
|
|
16
|
+
1..segCount single dirty segment k
|
|
17
|
+
segCount + FULL_OFFSET FULL payload
|
|
18
|
+
segCount + MULTI_OFFSET MULTI: bitmap + dirty segs in order
|
|
19
|
+
deltaArray and deltaMap manage their own flag spaces.
|
|
19
20
|
]]
|
|
20
21
|
local DELTA_UNCHANGED = 0
|
|
21
|
-
local DELTA_FULL = 1
|
|
22
22
|
local DELTA_FULL_OFFSET = 1
|
|
23
23
|
local DELTA_MULTI_OFFSET = 2
|
|
24
24
|
|
|
@@ -37,9 +37,29 @@ local lshift = bit32.lshift
|
|
|
37
37
|
local readu8 = buffer.readu8
|
|
38
38
|
local writeu8 = buffer.writeu8
|
|
39
39
|
local bufCopy = buffer.copy
|
|
40
|
+
local bufLen = buffer.len
|
|
40
41
|
local rangeEqual = Buf.rangeEqual
|
|
41
42
|
local snapshot = Buf.snapshot
|
|
42
43
|
|
|
44
|
+
--[[
|
|
45
|
+
Reuse `existing` when it's already large enough, else snapshot via
|
|
46
|
+
Buf.snapshot. Avoids per-write buffer allocations on delta caches that
|
|
47
|
+
hover at a stable size.
|
|
48
|
+
]]
|
|
49
|
+
local function copyToBuf(existing: buffer?, src: buffer, srcOff: number, len: number): buffer
|
|
50
|
+
if existing and bufLen(existing) >= len then
|
|
51
|
+
bufCopy(existing, 0, src, srcOff, len)
|
|
52
|
+
return existing
|
|
53
|
+
end
|
|
54
|
+
return snapshot(src, len, srcOff)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
-- Update a `{raw, len}` cache shape from scratch bytes; reuses raw via copyToBuf.
|
|
58
|
+
local function refreshCache(cache: any, src: buffer, len: number): ()
|
|
59
|
+
cache.raw = copyToBuf(cache.raw, src, 0, len)
|
|
60
|
+
cache.len = len
|
|
61
|
+
end
|
|
62
|
+
|
|
43
63
|
-- Public -----------------------------------------------------------------
|
|
44
64
|
|
|
45
65
|
local Shared = {}
|
|
@@ -49,6 +69,20 @@ Shared.DELTA_FULL_OFFSET = DELTA_FULL_OFFSET
|
|
|
49
69
|
Shared.DELTA_MULTI_OFFSET = DELTA_MULTI_OFFSET
|
|
50
70
|
Shared.rangeEqual = rangeEqual
|
|
51
71
|
Shared.snapshot = snapshot
|
|
72
|
+
Shared.refreshCache = refreshCache
|
|
73
|
+
Shared.copyToBuf = copyToBuf
|
|
74
|
+
|
|
75
|
+
-- True when `c` itself is a delta codec OR contains one in its tree.
|
|
76
|
+
function Shared.containsDelta(c: Types.InternalCodec<any>): boolean
|
|
77
|
+
return c._isDelta == true or c._hasDelta == true
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
-- Common bounds check for variable-length container codecs.
|
|
81
|
+
function Shared.enforceMaxCount(len: number, maxCount: number?, label: string?): ()
|
|
82
|
+
if maxCount and len > maxCount then
|
|
83
|
+
Log.error(`{label or "count"} {len} exceeds max {maxCount}`)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
52
86
|
|
|
53
87
|
function Shared.allocDeltaId(): number
|
|
54
88
|
_nextDeltaId += 1
|
|
@@ -82,6 +116,26 @@ function Shared.releaseScratch(): ()
|
|
|
82
116
|
_scratchDepth -= 1
|
|
83
117
|
end
|
|
84
118
|
|
|
119
|
+
--[[
|
|
120
|
+
Reset module-private state. Called by Sandbox in tests so that a thrown
|
|
121
|
+
codec.write (which skips releaseScratch and leaves depth inflated) can't
|
|
122
|
+
leak into the next test's scratch acquisition. Also resets the deltaId
|
|
123
|
+
counter so test ordering doesn't bleed.
|
|
124
|
+
]]
|
|
125
|
+
function Shared.reset(): ()
|
|
126
|
+
_scratchDepth = 0
|
|
127
|
+
_nextDeltaId = 0
|
|
128
|
+
for i = 1, #_scratchStack do
|
|
129
|
+
local ch = _scratchStack[i]
|
|
130
|
+
ch.cursor = 0
|
|
131
|
+
ch.refCount = 0
|
|
132
|
+
if ch.refCount > 0 then
|
|
133
|
+
table.clear(ch.refs)
|
|
134
|
+
end
|
|
135
|
+
table.clear(ch.deltas)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
85
139
|
--[[
|
|
86
140
|
Split a struct schema into (dataKeys, dataCodecs, boolKeys), each sorted
|
|
87
141
|
alphabetically for deterministic wire layout.
|
|
@@ -234,79 +288,4 @@ function Shared.unpackBoolArray(
|
|
|
234
288
|
return byteCount
|
|
235
289
|
end
|
|
236
290
|
|
|
237
|
-
--[[
|
|
238
|
-
Build a byte-equality delta codec around any inner codec. Wire:
|
|
239
|
-
[DELTA_UNCHANGED] 1 byte
|
|
240
|
-
[DELTA_FULL] <inner-encoded payload> 1 + N bytes
|
|
241
|
-
Used by deltaArray and deltaMap. The FULL path encodes once into scratch
|
|
242
|
-
then bufCopies into the channel — avoids a double encode.
|
|
243
|
-
]]
|
|
244
|
-
function Shared.makeUnchangedOrFullDelta(
|
|
245
|
-
inner: Types.InternalCodec<any>,
|
|
246
|
-
deltaId: number,
|
|
247
|
-
Baseline: {
|
|
248
|
-
getCache: (number) -> any,
|
|
249
|
-
setCache: (number, any) -> (),
|
|
250
|
-
}
|
|
251
|
-
): Types.InternalCodec<any>
|
|
252
|
-
return table.freeze({
|
|
253
|
-
_isDelta = true,
|
|
254
|
-
|
|
255
|
-
write = function(ch: Types.ChannelState, value: any): ()
|
|
256
|
-
local scratch = Shared.acquireScratch()
|
|
257
|
-
inner.write(scratch, value)
|
|
258
|
-
local scratchLen = scratch.cursor
|
|
259
|
-
|
|
260
|
-
local cache = ch.deltas[deltaId]
|
|
261
|
-
if
|
|
262
|
-
cache
|
|
263
|
-
and scratchLen == cache.len
|
|
264
|
-
and rangeEqual(scratch.buff, 0, cache.raw, 0, scratchLen)
|
|
265
|
-
then
|
|
266
|
-
local cursor = ch.cursor
|
|
267
|
-
if cursor + 1 > ch.size then
|
|
268
|
-
alloc(ch, 1)
|
|
269
|
-
end
|
|
270
|
-
writeu8(ch.buff, cursor, DELTA_UNCHANGED)
|
|
271
|
-
ch.cursor = cursor + 1
|
|
272
|
-
Shared.releaseScratch()
|
|
273
|
-
return
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
local cursor = ch.cursor
|
|
277
|
-
if cursor + 1 + scratchLen > ch.size then
|
|
278
|
-
alloc(ch, 1 + scratchLen)
|
|
279
|
-
end
|
|
280
|
-
local b = ch.buff
|
|
281
|
-
writeu8(b, cursor, DELTA_FULL)
|
|
282
|
-
if scratchLen > 0 then
|
|
283
|
-
bufCopy(b, cursor + 1, scratch.buff, 0, scratchLen)
|
|
284
|
-
end
|
|
285
|
-
ch.cursor = cursor + 1 + scratchLen
|
|
286
|
-
|
|
287
|
-
local snap = snapshot(scratch.buff, scratchLen)
|
|
288
|
-
if cache then
|
|
289
|
-
cache.raw = snap
|
|
290
|
-
cache.len = scratchLen
|
|
291
|
-
else
|
|
292
|
-
ch.deltas[deltaId] = { raw = snap, len = scratchLen }
|
|
293
|
-
end
|
|
294
|
-
Shared.releaseScratch()
|
|
295
|
-
end,
|
|
296
|
-
|
|
297
|
-
read = function(src: buffer, pos: number, refs: { Instance }?): (any, number)
|
|
298
|
-
if readu8(src, pos) == DELTA_UNCHANGED then
|
|
299
|
-
local cached = Baseline.getCache(deltaId)
|
|
300
|
-
if not cached then
|
|
301
|
-
Log.error("UNCHANGED flag before any FULL frame; baseline missing")
|
|
302
|
-
end
|
|
303
|
-
return cached, 1
|
|
304
|
-
end
|
|
305
|
-
local value, consumed = inner.read(src, pos + 1, refs)
|
|
306
|
-
Baseline.setCache(deltaId, value)
|
|
307
|
-
return value, 1 + consumed
|
|
308
|
-
end,
|
|
309
|
-
})
|
|
310
|
-
end
|
|
311
|
-
|
|
312
291
|
return table.freeze(Shared)
|
|
@@ -55,6 +55,7 @@ local packBools = Shared.packBools
|
|
|
55
55
|
local unpackBools = Shared.unpackBools
|
|
56
56
|
local rangeEqual = Shared.rangeEqual
|
|
57
57
|
local snapshot = Shared.snapshot
|
|
58
|
+
local refreshCache = Shared.refreshCache
|
|
58
59
|
local acquireScratch = Shared.acquireScratch
|
|
59
60
|
local releaseScratch = Shared.releaseScratch
|
|
60
61
|
local allocDeltaId = Shared.allocDeltaId
|
|
@@ -89,14 +90,18 @@ function Struct.struct(schema: { [string]: Types.InternalCodec<any> }): Types.In
|
|
|
89
90
|
local boolCount = #boolKeys
|
|
90
91
|
local boolBytes = Shared.boolBytes(boolCount)
|
|
91
92
|
|
|
92
|
-
-- Single pass: detect all-direct fast-write path + propagate _hasUnknown.
|
|
93
|
+
-- Single pass: detect all-direct fast-write path + propagate _hasUnknown / _hasDelta.
|
|
93
94
|
local allDirect, fixedSize = true, 0
|
|
94
95
|
local hasUnknown = false
|
|
96
|
+
local hasDelta = false
|
|
95
97
|
for i = 1, dataCount do
|
|
96
98
|
local c = dataCodecs[i]
|
|
97
99
|
if c._hasUnknown then
|
|
98
100
|
hasUnknown = true
|
|
99
101
|
end
|
|
102
|
+
if Shared.containsDelta(c) then
|
|
103
|
+
hasDelta = true
|
|
104
|
+
end
|
|
100
105
|
if allDirect then
|
|
101
106
|
if c._size and c._directWrite and c._directRead then
|
|
102
107
|
fixedSize += c._size
|
|
@@ -194,6 +199,9 @@ function Struct.struct(schema: { [string]: Types.InternalCodec<any> }): Types.In
|
|
|
194
199
|
if hasUnknown then
|
|
195
200
|
codec._hasUnknown = true
|
|
196
201
|
end
|
|
202
|
+
if hasDelta then
|
|
203
|
+
codec._hasDelta = true
|
|
204
|
+
end
|
|
197
205
|
return table.freeze(codec)
|
|
198
206
|
end
|
|
199
207
|
|
|
@@ -226,6 +234,9 @@ function Struct.struct(schema: { [string]: Types.InternalCodec<any> }): Types.In
|
|
|
226
234
|
if hasUnknown then
|
|
227
235
|
codec._hasUnknown = true
|
|
228
236
|
end
|
|
237
|
+
if hasDelta then
|
|
238
|
+
codec._hasDelta = true
|
|
239
|
+
end
|
|
229
240
|
return table.freeze(codec)
|
|
230
241
|
end
|
|
231
242
|
|
|
@@ -421,8 +432,7 @@ function Struct.deltaStruct(
|
|
|
421
432
|
end
|
|
422
433
|
end
|
|
423
434
|
|
|
424
|
-
cache
|
|
425
|
-
cache.len = scratchLen;
|
|
435
|
+
refreshCache(cache, scratch.buff, scratchLen);
|
|
426
436
|
(cache :: any).segOff = segOff;
|
|
427
437
|
(cache :: any).segLen = segLen
|
|
428
438
|
releaseScratch()
|