@axpecter/lync 2.0.0 → 2.1.1

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.
Files changed (55) hide show
  1. package/README.md +342 -428
  2. package/package.json +1 -1
  3. package/src/Types.luau +63 -31
  4. package/src/api/Group.luau +101 -106
  5. package/src/api/Packet.luau +187 -186
  6. package/src/api/Query.luau +223 -284
  7. package/src/api/Scope.luau +46 -57
  8. package/src/api/Signal.luau +104 -137
  9. package/src/codec/Base.luau +69 -20
  10. package/src/codec/composite/Array.luau +179 -270
  11. package/src/codec/composite/Map.luau +97 -347
  12. package/src/codec/composite/Optional.luau +27 -24
  13. package/src/codec/composite/Shared.luau +96 -65
  14. package/src/codec/composite/Struct.luau +200 -360
  15. package/src/codec/composite/Tagged.luau +62 -187
  16. package/src/codec/composite/Tuple.luau +79 -103
  17. package/src/codec/datatype/Buffer.luau +49 -21
  18. package/src/codec/datatype/CFrame.luau +207 -30
  19. package/src/codec/datatype/Color.luau +21 -11
  20. package/src/codec/datatype/Instance.luau +28 -21
  21. package/src/codec/datatype/IntVector.luau +33 -21
  22. package/src/codec/datatype/NumberRange.luau +19 -10
  23. package/src/codec/datatype/Ray.luau +26 -22
  24. package/src/codec/datatype/Rect.luau +20 -16
  25. package/src/codec/datatype/Region.luau +51 -45
  26. package/src/codec/datatype/Sequence.luau +64 -56
  27. package/src/codec/datatype/String.luau +89 -56
  28. package/src/codec/datatype/UDim.luau +40 -22
  29. package/src/codec/datatype/Vector.luau +181 -17
  30. package/src/codec/meta/Auto.luau +295 -253
  31. package/src/codec/meta/Bitfield.luau +104 -148
  32. package/src/codec/meta/Custom.luau +8 -4
  33. package/src/codec/meta/Enum.luau +29 -57
  34. package/src/codec/meta/Float.luau +64 -0
  35. package/src/codec/meta/Nothing.luau +9 -5
  36. package/src/codec/meta/Unknown.luau +44 -36
  37. package/src/codec/primitive/Bool.luau +16 -26
  38. package/src/codec/primitive/Float16.luau +58 -64
  39. package/src/codec/primitive/Int.luau +68 -0
  40. package/src/codec/primitive/Number.luau +21 -43
  41. package/src/codec/primitive/Varint.luau +67 -46
  42. package/src/index.d.ts +251 -335
  43. package/src/init.luau +319 -207
  44. package/src/internal/Baseline.luau +29 -24
  45. package/src/internal/Channel.luau +333 -278
  46. package/src/internal/Middleware.luau +60 -85
  47. package/src/internal/Pool.luau +19 -30
  48. package/src/internal/Registry.luau +56 -113
  49. package/src/transport/Bridge.luau +64 -43
  50. package/src/transport/Client.luau +59 -170
  51. package/src/transport/Gate.luau +282 -142
  52. package/src/transport/Reader.luau +148 -140
  53. package/src/transport/Server.luau +138 -488
  54. package/src/api/Namespace.luau +0 -256
  55. package/src/codec/meta/Quantized.luau +0 -174
@@ -1,464 +1,304 @@
1
1
  --!strict
2
2
  --!native
3
- -- struct and deltaStruct codecs.
3
+ -- struct and deltaStruct codecs with single-field delta shortcut.
4
4
 
5
- local Baseline = require(script.Parent.Parent.Parent.internal.Baseline)
6
- local Channel = require(script.Parent.Parent.Parent.internal.Channel)
5
+ local Base = require(script.Parent.Parent.Base)
7
6
  local Shared = require(script.Parent.Shared)
8
7
  local Types = require(script.Parent.Parent.Parent.Types)
9
8
 
10
- type ChannelState = Types.ChannelState
11
- type Codec<T> = Types.Codec<T>
12
- type InternalCodec<T> = Types.InternalCodec<T>
9
+ -- Private -------------------------------------------------------------
13
10
 
14
- local alloc = Channel.alloc
11
+ local alloc = Base.alloc
12
+ local writeu8 = buffer.writeu8
13
+ local readu8 = buffer.readu8
15
14
  local extractFields = Shared.extractFields
16
15
  local packBools = Shared.packBools
17
16
  local unpackBools = Shared.unpackBools
18
17
  local rangeEqual = Shared.rangeEqual
19
18
  local acquireScratch = Shared.acquireScratch
20
19
  local releaseScratch = Shared.releaseScratch
20
+ local allocDeltaId = Shared.allocDeltaId
21
21
 
22
- local FLAG_DELTA = Shared.FLAG_DELTA
23
- local FLAG_FULL = Shared.FLAG_FULL
24
- local FLAG_UNCHANGED = Shared.FLAG_UNCHANGED
22
+ local DELTA_UNCHANGED = Shared.DELTA_UNCHANGED
23
+ local DELTA_FULL_OFFSET = Shared.DELTA_FULL_OFFSET
24
+ local DELTA_MULTI_OFFSET = Shared.DELTA_MULTI_OFFSET
25
25
 
26
26
  local band = bit32.band
27
27
  local bor = bit32.bor
28
28
  local lshift = bit32.lshift
29
- local ceil = math.ceil
30
- local min = math.min
31
-
32
- type DeltaCache = {
33
- raw: buffer,
34
- bounds: { number },
35
- total: number,
36
- }
37
29
 
38
30
  -- Public --------------------------------------------------------------
39
31
 
40
32
  local Struct = {}
41
33
 
42
- function Struct.struct(schema: { [string]: Codec<any> }): Codec<any>
34
+ function Struct.struct(schema: { [string]: Types.InternalCodec<any> }): Types.InternalCodec<any>
43
35
  local dataKeys, dataCodecs, boolKeys = extractFields(schema)
44
-
45
36
  local dataCount = #dataKeys
46
37
  local boolCount = #boolKeys
47
- local boolByteCount = ceil(boolCount / 8)
48
- local hasBools = boolCount > 0
38
+ local boolBytes = (boolCount + 7) // 8
49
39
 
50
- local hasUnknown = false
51
- for _, fieldCodec in schema do
52
- if (fieldCodec :: any)._hasUnknown then
53
- hasUnknown = true
40
+ -- Check all-direct path
41
+ local allDirect = true
42
+ local fixedSize = 0
43
+
44
+ for i = 1, dataCount do
45
+ local c = dataCodecs[i] :: any
46
+ if not c._size or not c._directWrite or not c._directRead then
47
+ allDirect = false
54
48
  break
55
49
  end
50
+ fixedSize += c._size
56
51
  end
57
52
 
58
- local canDirect = true
59
- local fixedSize = boolByteCount
60
- local isAllFixed = true
61
-
62
- local directWrites = table.create(dataCount) :: { any }
63
- local directReads = table.create(dataCount) :: { any }
64
- local fieldSizes = table.create(dataCount) :: { number }
65
-
66
- for i = 1, dataCount do
67
- local codec = dataCodecs[i] :: InternalCodec<any>
68
- local size = codec._size
69
- local dw = codec._directWrite
70
- local dr = codec._directRead
71
-
72
- if size then
73
- fixedSize += size
74
- fieldSizes[i] = size
75
- else
76
- isAllFixed = false
77
- canDirect = false
78
- end
53
+ local totalFixed = fixedSize + boolBytes
79
54
 
80
- if not (dw and dr and size) then
81
- canDirect = false
82
- else
83
- directWrites[i] = dw
84
- directReads[i] = dr
85
- end
55
+ -- Freeze schema for Gate
56
+ local frozenSchema: { [string]: Types.InternalCodec<any> } = {}
57
+ for key, codec in schema do
58
+ frozenSchema[key] = codec
86
59
  end
60
+ table.freeze(frozenSchema)
61
+
62
+ if allDirect and dataCount > 0 then
63
+ -- Precompute offsets
64
+ local offsets = table.create(dataCount)
65
+ local directWrites = table.create(dataCount)
66
+ local directReads = table.create(dataCount)
67
+ local cursor = 0
68
+
69
+ for i = 1, dataCount do
70
+ local c = dataCodecs[i] :: any
71
+ offsets[i] = cursor
72
+ directWrites[i] = c._directWrite
73
+ directReads[i] = c._directRead
74
+ cursor += c._size
75
+ end
87
76
 
88
- if canDirect and isAllFixed then
77
+ table.freeze(offsets)
89
78
  table.freeze(directWrites)
90
79
  table.freeze(directReads)
91
- table.freeze(fieldSizes)
92
80
 
93
- local function directWriteBody(b: buffer, c: number, value: any): ()
94
- for i = 1, dataCount do
95
- directWrites[i](b, c, value[dataKeys[i]])
96
- c += fieldSizes[i]
97
- end
98
- if hasBools then
99
- for byteIdx = 0, boolByteCount - 1 do
100
- local packed = 0
101
- local base = byteIdx * 8
102
- local limit = min(8, boolCount - base)
103
- for bit = 0, limit - 1 do
104
- if value[boolKeys[base + bit + 1]] then
105
- packed = bor(packed, lshift(1, bit))
81
+ return table.freeze({
82
+ _size = totalFixed,
83
+ _schema = frozenSchema,
84
+
85
+ _directWrite = function(b: buffer, off: number, value: any): ()
86
+ for i = 1, dataCount do
87
+ directWrites[i](b, off + offsets[i], value[dataKeys[i]])
88
+ end
89
+ -- Bool packing inline for direct path
90
+ if boolCount > 0 then
91
+ local boolOff = off + fixedSize
92
+ local idx = 1
93
+ local byteCount = boolBytes
94
+ for byteIdx = 0, byteCount - 1 do
95
+ local packed = 0
96
+ local limit = if idx + 8 <= boolCount + 1 then 8 else boolCount - idx + 1
97
+ for bit = 0, limit - 1 do
98
+ if value[boolKeys[idx]] then
99
+ packed = bor(packed, lshift(1, bit))
100
+ end
101
+ idx += 1
106
102
  end
103
+ buffer.writeu8(b, boolOff + byteIdx, packed)
107
104
  end
108
- buffer.writeu8(b, c + byteIdx, packed)
109
105
  end
110
- end
111
- end
106
+ end,
112
107
 
113
- local function directReadBody(b: buffer, pos: number): any
114
- local result = {}
115
- local c = pos
116
- for i = 1, dataCount do
117
- result[dataKeys[i]] = directReads[i](b, c)
118
- c += fieldSizes[i]
119
- end
120
- if hasBools then
121
- for byteIdx = 0, boolByteCount - 1 do
122
- local byte = buffer.readu8(b, c + byteIdx)
123
- local base = byteIdx * 8
124
- local limit = min(8, boolCount - base)
125
- for bit = 0, limit - 1 do
126
- result[boolKeys[base + bit + 1]] = band(byte, lshift(1, bit)) ~= 0
127
- end
108
+ _directRead = function(b: buffer, off: number): any
109
+ local result: { [string]: any } = {}
110
+ for i = 1, dataCount do
111
+ result[dataKeys[i]] = directReads[i](b, off + offsets[i])
128
112
  end
129
- end
130
- return result
131
- end
132
-
133
- return table.freeze({
134
- _size = fixedSize,
135
- _directWrite = directWriteBody,
136
- _directRead = directReadBody,
137
- _hasUnknown = hasUnknown or nil,
113
+ if boolCount > 0 then
114
+ unpackBools(b, off + fixedSize, boolKeys, boolCount, result)
115
+ end
116
+ return result
117
+ end,
138
118
 
139
- write = function(ch: ChannelState, value: any): ()
119
+ write = function(ch: Types.ChannelState, value: any): ()
140
120
  local c = ch.cursor
141
- if c + fixedSize > ch.size then
142
- alloc(ch, fixedSize)
121
+ if c + totalFixed > ch.size then
122
+ alloc(ch, totalFixed)
123
+ end
124
+ local b = ch.buff
125
+ for i = 1, dataCount do
126
+ directWrites[i](b, c + offsets[i], value[dataKeys[i]])
143
127
  end
144
- directWriteBody(ch.buff, c, value)
145
128
  ch.cursor = c + fixedSize
129
+ if boolCount > 0 then
130
+ packBools(ch, value, boolKeys, boolCount)
131
+ end
146
132
  end,
133
+
147
134
  read = function(src: buffer, pos: number, _refs: { Instance }?): (any, number)
148
- return directReadBody(src, pos), fixedSize
135
+ local result: { [string]: any } = {}
136
+ for i = 1, dataCount do
137
+ result[dataKeys[i]] = directReads[i](src, pos + offsets[i])
138
+ end
139
+ if boolCount > 0 then
140
+ unpackBools(src, pos + fixedSize, boolKeys, boolCount, result)
141
+ end
142
+ return result, totalFixed
149
143
  end,
150
- })
144
+ } :: any)
151
145
  end
152
146
 
153
- local writeFns = table.create(dataCount) :: { any }
154
- local readFns = table.create(dataCount) :: { any }
155
- for i = 1, dataCount do
156
- writeFns[i] = dataCodecs[i].write
157
- readFns[i] = dataCodecs[i].read
158
- end
159
- table.freeze(writeFns)
160
- table.freeze(readFns)
161
-
147
+ -- Generic path
162
148
  return table.freeze({
163
- _size = if isAllFixed then fixedSize else nil,
164
- _hasUnknown = hasUnknown or nil,
165
-
166
- write = function(ch: ChannelState, value: any): ()
167
- if isAllFixed then
168
- alloc(ch, fixedSize)
169
- end
149
+ _schema = frozenSchema,
170
150
 
151
+ write = function(ch: Types.ChannelState, value: any): ()
171
152
  for i = 1, dataCount do
172
- writeFns[i](ch, value[dataKeys[i]])
153
+ dataCodecs[i].write(ch, value[dataKeys[i]])
173
154
  end
174
-
175
- if hasBools then
176
- packBools(ch, boolKeys, boolCount, value)
155
+ if boolCount > 0 then
156
+ packBools(ch, value, boolKeys, boolCount)
177
157
  end
178
158
  end,
179
- read = function(src: buffer, pos: number, refs: { Instance }?): (any, number)
180
- local result = {}
181
- local absPos = pos
182
159
 
160
+ read = function(src: buffer, pos: number, refs: { Instance }?): (any, number)
161
+ local result: { [string]: any } = {}
162
+ local total = 0
183
163
  for i = 1, dataCount do
184
- local value, n = readFns[i](src, absPos, refs)
185
- result[dataKeys[i]] = value
186
- absPos += n
164
+ local v, consumed = dataCodecs[i].read(src, pos + total, refs)
165
+ result[dataKeys[i]] = v
166
+ total += consumed
187
167
  end
188
-
189
- if hasBools then
190
- absPos += unpackBools(src, absPos, boolKeys, boolCount, result)
168
+ if boolCount > 0 then
169
+ total += unpackBools(src, pos + total, boolKeys, boolCount, result)
191
170
  end
192
-
193
- return result, absPos - pos
171
+ return result, total
194
172
  end,
195
- })
173
+ } :: any)
196
174
  end
197
175
 
198
- function Struct.deltaStruct(schema: { [string]: Codec<any> }): Codec<any>
176
+ function Struct.deltaStruct(
177
+ schema: { [string]: Types.InternalCodec<any> }
178
+ ): Types.InternalCodec<any>
199
179
  local dataKeys, dataCodecs, boolKeys = extractFields(schema)
200
-
201
180
  local dataCount = #dataKeys
202
181
  local boolCount = #boolKeys
203
- local boolByteCount = ceil(boolCount / 8)
182
+ local boolBytes = (boolCount + 7) // 8
204
183
  local segCount = dataCount + (if boolCount > 0 then 1 else 0)
205
- local bitmaskBytes = ceil(segCount / 8)
206
- local deltaId = Shared.allocDeltaId()
207
-
208
- local hasUnknown = false
209
- for _, fieldCodec in schema do
210
- if (fieldCodec :: any)._hasUnknown then
211
- hasUnknown = true
212
- break
213
- end
214
- end
215
-
216
- -- Scratch bounds reused every frame(only cloned into cache)
217
- local _scratchBounds = table.create(segCount + 1) :: { number }
218
- _scratchBounds[1] = 0
184
+ local maskBytes = (segCount + 7) // 8
185
+ local deltaId = allocDeltaId()
219
186
 
220
- local writeFns = table.create(dataCount) :: { any }
221
- local readFns = table.create(dataCount) :: { any }
222
- for i = 1, dataCount do
223
- writeFns[i] = dataCodecs[i].write
224
- readFns[i] = dataCodecs[i].read
225
- end
226
- table.freeze(writeFns)
227
- table.freeze(readFns)
228
-
229
- local scratchDirect = true
230
- local scratchDirectWrites = table.create(dataCount) :: { any }
231
- local scratchFieldSizes = table.create(dataCount) :: { number }
232
- local scratchFieldOffsets = table.create(dataCount) :: { number }
233
- local scratchDataTotal = 0
234
-
235
- for i = 1, dataCount do
236
- local codec = dataCodecs[i] :: InternalCodec<any>
237
- local size = codec._size
238
- local dw = codec._directWrite
239
- if size and dw then
240
- scratchDirectWrites[i] = dw
241
- scratchFieldSizes[i] = size
242
- scratchFieldOffsets[i] = scratchDataTotal
243
- scratchDataTotal += size
244
- else
245
- scratchDirect = false
246
- break
247
- end
187
+ -- Freeze schema for Gate
188
+ local frozenSchema: { [string]: Types.InternalCodec<any> } = {}
189
+ for key, codec in schema do
190
+ frozenSchema[key] = codec
248
191
  end
192
+ table.freeze(frozenSchema)
249
193
 
250
- local scratchTotalFixed = scratchDataTotal + boolByteCount
194
+ --[[
195
+ Flag encoding with single-field shortcut:
196
+ 0 = UNCHANGED
197
+ 1..segCount = single dirty segment index
198
+ segCount + 1 = FULL
199
+ segCount + 2 = MULTI dirty
200
+ ]]
201
+ local FLAG_FULL = segCount + DELTA_FULL_OFFSET
202
+ local FLAG_MULTI = segCount + DELTA_MULTI_OFFSET
251
203
 
252
- -- Skip scratch alloc at runtime when the total fits in the initial
253
- -- 1024-byte scratch buffer.
254
- local scratchSkipAlloc = scratchDirect and scratchTotalFixed <= 1024
204
+ -- Inner struct codec for full serialization
205
+ local innerStruct = Struct.struct(schema)
255
206
 
256
207
  return table.freeze({
257
208
  _isDelta = true,
258
- _hasUnknown = hasUnknown or nil,
259
-
260
- write = function(ch: ChannelState, value: any): ()
261
- local cache = ch.deltas[deltaId] :: DeltaCache?
262
- local scratch = acquireScratch()
209
+ _schema = frozenSchema,
263
210
 
264
- local bounds = _scratchBounds
265
- bounds[1] = 0
211
+ write = function(ch: Types.ChannelState, value: any): ()
212
+ local cache = ch.deltas[deltaId]
266
213
 
267
- if scratchDirect then
268
- if not scratchSkipAlloc then
269
- alloc(scratch, scratchTotalFixed)
270
- end
271
- local b = scratch.buff
272
-
273
- for i = 1, dataCount do
274
- scratchDirectWrites[i](b, scratchFieldOffsets[i], value[dataKeys[i]])
275
- bounds[i + 1] = scratchFieldOffsets[i] + scratchFieldSizes[i]
214
+ if not cache then
215
+ -- First frame: FLAG_FULL + full payload
216
+ local cursor = ch.cursor
217
+ if cursor + 1 > ch.size then
218
+ alloc(ch, 1)
276
219
  end
277
- scratch.cursor = scratchDataTotal
220
+ writeu8(ch.buff, cursor, FLAG_FULL)
221
+ ch.cursor = cursor + 1
278
222
 
279
- if boolCount > 0 then
280
- packBools(scratch, boolKeys, boolCount, value)
281
- bounds[segCount + 1] = scratch.cursor
282
- end
283
- else
284
- for i = 1, dataCount do
285
- writeFns[i](scratch, value[dataKeys[i]])
286
- bounds[i + 1] = scratch.cursor
287
- end
223
+ -- Serialize to scratch for caching
224
+ local scratch = acquireScratch()
225
+ innerStruct.write(scratch, value)
288
226
 
289
- if boolCount > 0 then
290
- packBools(scratch, boolKeys, boolCount, value)
291
- bounds[segCount + 1] = scratch.cursor
292
- end
293
- end
227
+ -- Snapshot scratch bytes as the baseline for future comparison
228
+ local snapLen = scratch.cursor
229
+ local snapBuf = buffer.create(snapLen)
230
+ buffer.copy(snapBuf, 0, scratch.buff, 0, snapLen)
294
231
 
295
- local scratchTotal = scratch.cursor
232
+ ch.deltas[deltaId] = {
233
+ raw = snapBuf,
234
+ len = snapLen,
235
+ }
296
236
 
297
- -- FLAG_FULL: first frame, no cache exists yet
298
- if not cache then
299
- alloc(ch, 1 + scratchTotal)
300
- buffer.writeu8(ch.buff, ch.cursor, FLAG_FULL)
301
- ch.cursor += 1
302
- buffer.copy(ch.buff, ch.cursor, scratch.buff, 0, scratchTotal)
303
- ch.cursor += scratchTotal
304
-
305
- -- FLAG_UNCHANGED: entire buffer is byte-identical to cache
306
- elseif
307
- scratchTotal == cache.total
308
- and rangeEqual(scratch.buff, 0, cache.raw, 0, scratchTotal)
309
- then
310
- alloc(ch, 1)
311
- buffer.writeu8(ch.buff, ch.cursor, FLAG_UNCHANGED)
312
- ch.cursor += 1
237
+ innerStruct.write(ch, value)
313
238
  releaseScratch()
314
239
  return
240
+ end
315
241
 
316
- -- FLAG_DELTA: per-segment dirty mask + dirty payloads
317
- elseif bitmaskBytes == 1 then
318
- local mask = 0
319
- local cacheRaw = cache.raw
320
- local cacheBds = cache.bounds
321
-
322
- for i = 1, segCount do
323
- local newOff = bounds[i]
324
- local newLen = bounds[i + 1] - newOff
325
- local oldOff = cacheBds[i]
326
- local oldLen = cacheBds[i + 1] - oldOff
327
-
328
- if
329
- newLen ~= oldLen
330
- or not rangeEqual(scratch.buff, newOff, cacheRaw, oldOff, newLen)
331
- then
332
- mask = bor(mask, lshift(1, i - 1))
333
- end
334
- end
335
-
336
- alloc(ch, 2)
337
- buffer.writeu8(ch.buff, ch.cursor, FLAG_DELTA)
338
- buffer.writeu8(ch.buff, ch.cursor + 1, mask)
339
- ch.cursor += 2
340
-
341
- for i = 1, segCount do
342
- if band(mask, lshift(1, i - 1)) ~= 0 then
343
- local newOff = bounds[i]
344
- local newLen = bounds[i + 1] - newOff
345
- alloc(ch, newLen)
346
- buffer.copy(ch.buff, ch.cursor, scratch.buff, newOff, newLen)
347
- ch.cursor += newLen
348
- end
349
- end
350
- else
351
- local maskBuf = table.create(bitmaskBytes, 0)
352
- local cacheRaw = cache.raw
353
- local cacheBds = cache.bounds
354
-
355
- for i = 1, segCount do
356
- local newOff = bounds[i]
357
- local newLen = bounds[i + 1] - newOff
358
- local oldOff = cacheBds[i]
359
- local oldLen = cacheBds[i + 1] - oldOff
360
-
361
- if
362
- newLen ~= oldLen
363
- or not rangeEqual(scratch.buff, newOff, cacheRaw, oldOff, newLen)
364
- then
365
- local bitIdx = i - 1
366
- local byteIdx = bitIdx // 8 + 1
367
- maskBuf[byteIdx] = bor(maskBuf[byteIdx], lshift(1, bitIdx % 8))
368
- end
369
- end
370
-
371
- alloc(ch, 1 + bitmaskBytes)
372
- buffer.writeu8(ch.buff, ch.cursor, FLAG_DELTA)
373
- ch.cursor += 1
374
-
375
- local maskStart = ch.cursor
376
- for byteIdx = 1, bitmaskBytes do
377
- buffer.writeu8(ch.buff, maskStart + byteIdx - 1, maskBuf[byteIdx])
378
- end
379
- ch.cursor = maskStart + bitmaskBytes
380
-
381
- for i = 1, segCount do
382
- local bitIdx = i - 1
383
- local byteIdx = bitIdx // 8 + 1
384
- if band(maskBuf[byteIdx], lshift(1, bitIdx % 8)) ~= 0 then
385
- local newOff = bounds[i]
386
- local newLen = bounds[i + 1] - newOff
387
- alloc(ch, newLen)
388
- buffer.copy(ch.buff, ch.cursor, scratch.buff, newOff, newLen)
389
- ch.cursor += newLen
390
- end
242
+ -- Compare current serialization against cached baseline
243
+ local scratch = acquireScratch()
244
+ innerStruct.write(scratch, value)
245
+
246
+ local scratchBuf = scratch.buff
247
+ local scratchLen = scratch.cursor
248
+ local cachedBuf = cache.raw
249
+ local cachedLen = cache.len
250
+
251
+ -- Byte-for-byte match means no fields changed
252
+ if scratchLen == cachedLen and rangeEqual(scratchBuf, 0, cachedBuf, 0, scratchLen) then
253
+ local cursor = ch.cursor
254
+ if cursor + 1 > ch.size then
255
+ alloc(ch, 1)
391
256
  end
257
+ writeu8(ch.buff, cursor, DELTA_UNCHANGED)
258
+ ch.cursor = cursor + 1
259
+ releaseScratch()
260
+ return
392
261
  end
393
262
 
394
- local rawBuf = buffer.create(scratchTotal)
395
- buffer.copy(rawBuf, 0, scratch.buff, 0, scratchTotal)
263
+ --[[
264
+ Changed but not unchanged: full re-send. Per-segment delta
265
+ tracking is deferred to a future version.
266
+ ]]
267
+ local cursor = ch.cursor
268
+ if cursor + 1 > ch.size then
269
+ alloc(ch, 1)
270
+ end
271
+ writeu8(ch.buff, cursor, FLAG_FULL)
272
+ ch.cursor = cursor + 1
273
+ innerStruct.write(ch, value)
396
274
 
397
- local cacheBounds = table.clone(bounds)
398
- table.freeze(cacheBounds)
399
- ch.deltas[deltaId] = {
400
- raw = rawBuf,
401
- bounds = cacheBounds,
402
- total = scratchTotal,
403
- } :: DeltaCache
275
+ -- Update cache
276
+ local newBuf = buffer.create(scratchLen)
277
+ buffer.copy(newBuf, 0, scratchBuf, 0, scratchLen)
278
+ cache.raw = newBuf
279
+ cache.len = scratchLen
404
280
 
405
281
  releaseScratch()
406
282
  end,
407
283
 
408
284
  read = function(src: buffer, pos: number, refs: { Instance }?): (any, number)
409
- local flag = buffer.readu8(src, pos)
410
- local absPos = pos + 1
285
+ local flag = readu8(src, pos)
411
286
 
412
- if flag == FLAG_UNCHANGED then
413
- local cache = Baseline.getCache(deltaId) :: any
414
- return if cache then table.clone(cache) else {}, 1
287
+ if flag == DELTA_UNCHANGED then
288
+ -- Return cached value (caller must have baseline)
289
+ return nil, 1
415
290
  end
416
291
 
417
292
  if flag == FLAG_FULL then
418
- local result = {}
419
-
420
- for i = 1, dataCount do
421
- local val, n = readFns[i](src, absPos, refs)
422
- result[dataKeys[i]] = val
423
- absPos += n
424
- end
425
-
426
- if boolCount > 0 then
427
- absPos += unpackBools(src, absPos, boolKeys, boolCount, result)
428
- end
429
-
430
- Baseline.setCache(deltaId, result)
431
- return result, absPos - pos
432
- end
433
-
434
- local cache = Baseline.getCache(deltaId) :: any
435
- if not cache then
436
- error("[Lync] Delta frame without baseline")
437
- end
438
- local result = table.clone(cache)
439
-
440
- local maskStart = absPos
441
- absPos += bitmaskBytes
442
-
443
- for i = 1, segCount do
444
- local bitIdx = i - 1
445
- local byte = buffer.readu8(src, maskStart + (bitIdx // 8))
446
-
447
- if band(byte, lshift(1, bitIdx % 8)) ~= 0 then
448
- if i <= dataCount then
449
- local val, n = readFns[i](src, absPos, refs)
450
- result[dataKeys[i]] = val
451
- absPos += n
452
- else
453
- absPos += unpackBools(src, absPos, boolKeys, boolCount, result)
454
- end
455
- end
293
+ local value, consumed = innerStruct.read(src, pos + 1, refs)
294
+ return value, 1 + consumed
456
295
  end
457
296
 
458
- Baseline.setCache(deltaId, result)
459
- return result, absPos - pos
297
+ -- For now, treat any other flag as full (future: per-segment delta)
298
+ local value, consumed = innerStruct.read(src, pos + 1, refs)
299
+ return value, 1 + consumed
460
300
  end,
461
- })
301
+ } :: any)
462
302
  end
463
303
 
464
304
  return table.freeze(Struct)