@axpecter/lync 2.3.1 → 2.3.3

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.
@@ -9,15 +9,36 @@ 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
+ -- deltaArray wire flags. Distinct from Shared's struct/map flag spaces.
15
+ local DA_UNCHANGED = 0
16
+ local DA_FULL = 1
17
+ local DA_PATCH = 2
18
+
12
19
  -- Private ----------------------------------------------------------------
13
20
 
14
21
  local alloc = Base.alloc
22
+ local ensure = Base.ensure
15
23
  local allocDeltaId = Shared.allocDeltaId
24
+ local acquireScratch = Shared.acquireScratch
25
+ local releaseScratch = Shared.releaseScratch
26
+ local rangeEqual = Shared.rangeEqual
27
+ local copyToBuf = Shared.copyToBuf
28
+ local containsDelta = Shared.containsDelta
29
+ local enforceMaxCount = Shared.enforceMaxCount
16
30
  local boolBytes = Shared.boolBytes
17
31
  local packBoolArray = Shared.packBoolArray
18
32
  local unpackBoolArray = Shared.unpackBoolArray
19
33
  local varintRead = Varint.read
20
34
  local varintWrite = Varint.write
35
+ local varintLength = Varint.length
36
+ local writeu8 = buffer.writeu8
37
+ local readu8 = buffer.readu8
38
+ local bufCopy = buffer.copy
39
+ local bufLen = buffer.len
40
+
41
+ type DeltaArrayEntry = { eBuf: buffer, eLen: number }
21
42
 
22
43
  -- Bit-packed bool element fast path: 1 bit per entry instead of 1 byte.
23
44
  local function makeBoolArray(
@@ -32,25 +53,23 @@ local function makeBoolArray(
32
53
  write = function(ch: Types.ChannelState, value: { boolean }): ()
33
54
  local len = #value
34
55
  varintWrite(ch, len)
35
- packBoolArray(ch, value, len)
56
+ packBoolArray(ch, value, len, if len > 0 then boolBytes(len) else 0)
36
57
  end,
37
58
 
38
59
  read = function(src: buffer, pos: number, _refs: { Instance }?): ({ boolean }, number)
39
60
  local len, lenBytes = varintRead(src, pos)
40
- if maxCount and len > maxCount then
41
- Log.error(`count {len} exceeds max {maxCount}`)
42
- end
61
+ enforceMaxCount(len, maxCount)
43
62
  if len == 0 then
44
63
  return {}, lenBytes
45
64
  end
46
65
 
47
66
  local byteCount = boolBytes(len)
48
67
  local dataStart = pos + lenBytes
49
- if dataStart + byteCount > buffer.len(src) then
68
+ if dataStart + byteCount > bufLen(src) then
50
69
  Log.error(`payload {byteCount}B exceeds remaining buffer`)
51
70
  end
52
71
  local result = table.create(len, false)
53
- unpackBoolArray(src, dataStart, len, result)
72
+ unpackBoolArray(src, dataStart, len, result, byteCount)
54
73
  return result, lenBytes + byteCount
55
74
  end,
56
75
  }
@@ -92,16 +111,14 @@ local function makeDirectArray(
92
111
 
93
112
  read = function(src: buffer, pos: number, _refs: { Instance }?): ({ any }, number)
94
113
  local len, lenBytes = varintRead(src, pos)
95
- if maxCount and len > maxCount then
96
- Log.error(`count {len} exceeds max {maxCount}`)
97
- end
114
+ enforceMaxCount(len, maxCount)
98
115
  if len == 0 then
99
116
  return {}, lenBytes
100
117
  end
101
118
 
102
119
  local payloadBytes = len * elemSize
103
120
  local dataStart = pos + lenBytes
104
- if payloadBytes > buffer.len(src) - dataStart then
121
+ if payloadBytes > bufLen(src) - dataStart then
105
122
  Log.error(`payload {payloadBytes}B exceeds remaining buffer`)
106
123
  end
107
124
 
@@ -136,12 +153,15 @@ local function makeGenericArray(
136
153
 
137
154
  read = function(src: buffer, pos: number, refs: { Instance }?): ({ any }, number)
138
155
  local len, lenBytes = varintRead(src, pos)
139
- if maxCount and len > maxCount then
140
- Log.error(`count {len} exceeds max {maxCount}`)
141
- end
156
+ enforceMaxCount(len, maxCount)
142
157
  if len == 0 then
143
158
  return {}, lenBytes
144
159
  end
160
+ -- Every element occupies at least 1 byte; len > remaining is
161
+ -- unambiguously corrupt and would OOM table.create otherwise.
162
+ if len > bufLen(src) - (pos + lenBytes) then
163
+ Log.error(`array length {len} exceeds remaining buffer`)
164
+ end
145
165
 
146
166
  local result = table.create(len)
147
167
  local total = lenBytes
@@ -179,19 +199,275 @@ function Array.array(element: Types.InternalCodec<any>, maxCount: number?): Type
179
199
  if element._hasUnknown then
180
200
  codec._hasUnknown = true
181
201
  end
202
+ if containsDelta(element) then
203
+ codec._hasDelta = true
204
+ end
182
205
  return table.freeze(codec)
183
206
  end
184
207
 
185
208
  --[[
186
- UNCHANGED-or-FULL delta. Useful only when contents AND order are stable
187
- across frames; for unstable lists, plain array() avoids a 1-byte tax on
188
- every frame the byte-equality cache misses.
209
+ Per-index deltaArray. Wire:
210
+ [0] UNCHANGED (1 byte)
211
+ [1] FULL <count varint> <element bytes ...>
212
+ [2] PATCH <newLen varint> <changeCount varint>
213
+ <(idx varint, element bytes) ...>
214
+
215
+ Length shrinkage is implicit in newLen — entries past newLen drop from
216
+ the cached map. Length growth: appended indices show up as changes.
217
+ Reordered lists tend to mark every index changed; the writer falls back
218
+ to FULL when changeCount * 2 >= newLen since PATCH index varints eat
219
+ any savings past that ratio.
189
220
  ]]
190
221
  function Array.deltaArray(
191
222
  element: Types.InternalCodec<any>,
192
223
  maxCount: number?
193
224
  ): Types.InternalCodec<any>
194
- return Shared.makeUnchangedOrFullDelta(Array.array(element, maxCount), allocDeltaId(), Baseline)
225
+ -- Inner delta would chain-diff across elements within a frame, not across frames.
226
+ if containsDelta(element) then
227
+ Log.error("deltaArray element cannot contain delta state")
228
+ end
229
+
230
+ local deltaId = allocDeltaId()
231
+ local inner = Array.array(element, maxCount)
232
+
233
+ --[[
234
+ Encode each element into `scratch` and capture per-index byte ranges.
235
+ Returns the array length plus parallel arrays of offset/length so the
236
+ diff path can compare and emit individual entries without re-encoding.
237
+ ]]
238
+ local function encodePerIdx(
239
+ scratch: Types.ChannelState,
240
+ value: { any }
241
+ ): (number, { number }, { number })
242
+ local len = #value
243
+ enforceMaxCount(len, maxCount)
244
+ local off = table.create(len)
245
+ local elen = table.create(len)
246
+ for i = 1, len do
247
+ local before = scratch.cursor
248
+ element.write(scratch, value[i])
249
+ off[i] = before
250
+ elen[i] = scratch.cursor - before
251
+ end
252
+ return len, off, elen
253
+ end
254
+
255
+ return table.freeze({
256
+ _isDelta = true,
257
+ _isArray = true,
258
+ _element = element,
259
+ _maxCount = maxCount,
260
+
261
+ write = function(ch: Types.ChannelState, value: { 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 len, off, elen = encodePerIdx(scratch, value)
269
+ local scratchBuf = scratch.buff
270
+
271
+ -- First frame: emit FULL using the per-index scratch bytes.
272
+ if not cache then
273
+ ensure(ch, 1)
274
+ writeu8(ch.buff, ch.cursor, DA_FULL)
275
+ ch.cursor += 1
276
+ varintWrite(ch, len)
277
+ local payloadLen = scratch.cursor
278
+ ensure(ch, payloadLen)
279
+ bufCopy(ch.buff, ch.cursor, scratchBuf, 0, payloadLen)
280
+ ch.cursor += payloadLen
281
+
282
+ local perIdx: { [number]: DeltaArrayEntry } = {}
283
+ for i = 1, len do
284
+ local n = elen[i]
285
+ perIdx[i] = { eBuf = copyToBuf(nil, scratchBuf, off[i], n), eLen = n }
286
+ end
287
+ ch.deltas[deltaId] = { perIdx = perIdx, length = len } :: any
288
+ releaseScratch()
289
+ return
290
+ end
291
+
292
+ local cachedPerIdx = cache.perIdx :: { [number]: DeltaArrayEntry }
293
+ local cachedLen = cache.length :: number
294
+
295
+ -- Find changed indices in the overlap; appended indices count as changed.
296
+ local changed: { number } = {}
297
+ local changedCount = 0
298
+ local overlap = if len < cachedLen then len else cachedLen
299
+ for i = 1, overlap do
300
+ local entry = cachedPerIdx[i]
301
+ local n = elen[i]
302
+ if
303
+ not entry
304
+ or n ~= entry.eLen
305
+ or not rangeEqual(scratchBuf, off[i], entry.eBuf, 0, n)
306
+ then
307
+ changedCount += 1
308
+ changed[changedCount] = i
309
+ end
310
+ end
311
+ for i = overlap + 1, len do
312
+ changedCount += 1
313
+ changed[changedCount] = i
314
+ end
315
+
316
+ if changedCount == 0 and len == cachedLen then
317
+ ensure(ch, 1)
318
+ writeu8(ch.buff, ch.cursor, DA_UNCHANGED)
319
+ ch.cursor += 1
320
+ releaseScratch()
321
+ return
322
+ end
323
+
324
+ -- Pick FULL vs PATCH on actual byte cost. Both share `1 + varintLength(len)`
325
+ -- so it cancels: compare PATCH's index-header overhead + changed payload
326
+ -- against FULL's full payload.
327
+ local fullPayload = scratch.cursor
328
+ local patchPayload = varintLength(changedCount)
329
+ for i = 1, changedCount do
330
+ local idx = changed[i]
331
+ patchPayload += varintLength(idx) + elen[idx]
332
+ end
333
+
334
+ if patchPayload < fullPayload then
335
+ ensure(ch, 1)
336
+ writeu8(ch.buff, ch.cursor, DA_PATCH)
337
+ ch.cursor += 1
338
+ varintWrite(ch, len)
339
+ varintWrite(ch, changedCount)
340
+ for i = 1, changedCount do
341
+ local idx = changed[i]
342
+ varintWrite(ch, idx)
343
+ local n = elen[idx]
344
+ ensure(ch, n)
345
+ bufCopy(ch.buff, ch.cursor, scratchBuf, off[idx], n)
346
+ ch.cursor += n
347
+ end
348
+ else
349
+ ensure(ch, 1)
350
+ writeu8(ch.buff, ch.cursor, DA_FULL)
351
+ ch.cursor += 1
352
+ varintWrite(ch, len)
353
+ ensure(ch, fullPayload)
354
+ bufCopy(ch.buff, ch.cursor, scratchBuf, 0, fullPayload)
355
+ ch.cursor += fullPayload
356
+ end
357
+
358
+ -- Update cache: refresh changed (and appended), drop truncated.
359
+ for i = 1, changedCount do
360
+ local idx = changed[i]
361
+ local n = elen[idx]
362
+ local entry = cachedPerIdx[idx]
363
+ if entry then
364
+ entry.eBuf = copyToBuf(entry.eBuf, scratchBuf, off[idx], n)
365
+ entry.eLen = n
366
+ else
367
+ cachedPerIdx[idx] = { eBuf = copyToBuf(nil, scratchBuf, off[idx], n), eLen = n }
368
+ end
369
+ end
370
+ for i = len + 1, cachedLen do
371
+ cachedPerIdx[i] = nil
372
+ end
373
+ cache.length = len
374
+
375
+ releaseScratch()
376
+ end,
377
+
378
+ read = function(src: buffer, pos: number, refs: { Instance }?): ({ any }, number)
379
+ if pos >= bufLen(src) then
380
+ Log.error("truncated deltaArray header")
381
+ end
382
+ local flag = readu8(src, pos)
383
+
384
+ if flag == DA_UNCHANGED then
385
+ local cached = Baseline.getCache(deltaId)
386
+ if cached == nil then
387
+ Log.error("UNCHANGED flag before any FULL frame; baseline missing")
388
+ end
389
+ return cached, 1
390
+ end
391
+
392
+ if flag == DA_FULL then
393
+ local value, consumed = inner.read(src, pos + 1, refs)
394
+ Baseline.setCache(deltaId, value)
395
+ return value, 1 + consumed
396
+ end
397
+
398
+ if flag == DA_PATCH then
399
+ local cached = Baseline.getCache(deltaId)
400
+ if cached == nil then
401
+ Log.error("deltaArray PATCH before any FULL frame; baseline missing")
402
+ end
403
+ --[[
404
+ Capture the cached length BEFORE clone+mutate; #result
405
+ becomes unreliable once we set entries at appended indices
406
+ past the original boundary.
407
+ ]]
408
+ local cachedLen = #cached
409
+ local result: { any } = table.clone(cached)
410
+ local total = 1
411
+
412
+ local newLen, lLen = varintRead(src, pos + total)
413
+ if lLen == 0 then
414
+ Log.error("truncated deltaArray length")
415
+ end
416
+ enforceMaxCount(newLen, maxCount, "newLen")
417
+ total += lLen
418
+
419
+ local changeCount, cLen = varintRead(src, pos + total)
420
+ if cLen == 0 then
421
+ Log.error("truncated deltaArray change count")
422
+ end
423
+ if changeCount > newLen then
424
+ Log.error(`deltaArray changeCount {changeCount} exceeds newLen {newLen}`)
425
+ end
426
+ total += cLen
427
+
428
+ -- The writer marks every appended index (cachedLen+1 .. newLen)
429
+ -- as changed; the reader must enforce this so the wire can't
430
+ -- ship a holey baseline that breaks #result downstream.
431
+ local appendedNeeded = if newLen > cachedLen then newLen - cachedLen else 0
432
+ local appendedSeen = 0
433
+
434
+ for _ = 1, changeCount do
435
+ local idx, iLen = varintRead(src, pos + total)
436
+ if iLen == 0 then
437
+ Log.error("truncated deltaArray index")
438
+ end
439
+ if idx < 1 or idx > newLen then
440
+ Log.error(`deltaArray index {idx} out of range [1, {newLen}]`)
441
+ end
442
+ if idx > cachedLen then
443
+ appendedSeen += 1
444
+ end
445
+ total += iLen
446
+ local v, vc = element.read(src, pos + total, refs)
447
+ if vc == 0 then
448
+ Log.error("truncated deltaArray element")
449
+ end
450
+ total += vc
451
+ result[idx] = v
452
+ end
453
+
454
+ if appendedSeen ~= appendedNeeded then
455
+ Log.error(
456
+ `deltaArray PATCH grew length to {newLen} but only {appendedSeen}/{appendedNeeded} appended indices supplied`
457
+ )
458
+ end
459
+
460
+ for i = newLen + 1, cachedLen do
461
+ result[i] = nil
462
+ end
463
+
464
+ Baseline.setCache(deltaId, result)
465
+ return result, total
466
+ end
467
+
468
+ Log.error(`invalid deltaArray flag {flag}`)
469
+ end,
470
+ }) :: Types.InternalCodec<any>
195
471
  end
196
472
 
197
473
  return table.freeze(Array)