@axpecter/lync 2.2.0 → 2.3.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 (62) hide show
  1. package/README.md +95 -143
  2. package/package.json +1 -1
  3. package/src/Types.luau +22 -16
  4. package/src/api/Group.luau +40 -33
  5. package/src/api/Packet.luau +95 -72
  6. package/src/api/Query.luau +128 -79
  7. package/src/api/Scope.luau +26 -10
  8. package/src/api/Signal.luau +43 -52
  9. package/src/codec/Base.luau +21 -14
  10. package/src/codec/composite/Array.luau +56 -134
  11. package/src/codec/composite/Map.luau +103 -72
  12. package/src/codec/composite/Optional.luau +6 -2
  13. package/src/codec/composite/Shared.luau +160 -69
  14. package/src/codec/composite/Struct.luau +283 -42
  15. package/src/codec/composite/Tagged.luau +13 -16
  16. package/src/codec/composite/Tuple.luau +21 -15
  17. package/src/codec/datatype/Buffer.luau +6 -4
  18. package/src/codec/datatype/CFrame.luau +56 -44
  19. package/src/codec/datatype/Color.luau +10 -10
  20. package/src/codec/datatype/Instance.luau +15 -12
  21. package/src/codec/datatype/IntVector.luau +2 -2
  22. package/src/codec/datatype/NumberRange.luau +15 -8
  23. package/src/codec/datatype/Ray.luau +13 -14
  24. package/src/codec/datatype/Rect.luau +12 -12
  25. package/src/codec/datatype/Region.luau +13 -14
  26. package/src/codec/datatype/Sequence.luau +87 -65
  27. package/src/codec/datatype/String.luau +17 -10
  28. package/src/codec/datatype/UDim.luau +10 -8
  29. package/src/codec/datatype/Vector.luau +20 -59
  30. package/src/codec/meta/Auto.luau +94 -126
  31. package/src/codec/meta/Bitfield.luau +12 -14
  32. package/src/codec/meta/Custom.luau +3 -1
  33. package/src/codec/meta/Enum.luau +9 -9
  34. package/src/codec/meta/Float.luau +6 -28
  35. package/src/codec/meta/Nothing.luau +1 -1
  36. package/src/codec/meta/Unknown.luau +10 -7
  37. package/src/codec/primitive/Bool.luau +6 -4
  38. package/src/codec/primitive/Float16.luau +5 -2
  39. package/src/codec/primitive/Int.luau +5 -6
  40. package/src/codec/primitive/Number.luau +6 -4
  41. package/src/codec/primitive/Signed.luau +2 -2
  42. package/src/codec/primitive/Varint.luau +69 -38
  43. package/src/index.d.ts +161 -53
  44. package/src/init.luau +123 -102
  45. package/src/internal/Baseline.luau +9 -1
  46. package/src/internal/Channel.luau +153 -154
  47. package/src/internal/Middleware.luau +22 -6
  48. package/src/internal/Pool.luau +12 -4
  49. package/src/internal/Registry.luau +25 -16
  50. package/src/internal/Transport.luau +1 -1
  51. package/src/transport/Bridge.luau +47 -33
  52. package/src/transport/Client.luau +25 -21
  53. package/src/transport/Gate.luau +227 -172
  54. package/src/transport/Reader.luau +162 -105
  55. package/src/transport/Server.luau +46 -57
  56. package/src/util/Array.luau +18 -0
  57. package/src/util/Buffer.luau +92 -0
  58. package/src/util/Constants.luau +30 -0
  59. package/src/util/Log.luau +68 -0
  60. package/src/util/Player.luau +14 -0
  61. package/src/util/Quantize.luau +60 -0
  62. package/src/internal/Util.luau +0 -26
@@ -1,7 +1,6 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Batched connection lifecycle. Accepts Lync Connection,
4
- -- RBXScriptConnection, and any object with :disconnect() / :Disconnect().
3
+ -- Batched connection lifecycle for Lync, RBXScriptConnection, and disconnect()-style objects.
5
4
 
6
5
  local Types = require(script.Parent.Parent.Types)
7
6
 
@@ -25,21 +24,29 @@ local function pushConnection(self: Scope, conn: any): ()
25
24
  self._connections[idx] = conn
26
25
  end
27
26
 
27
+ -- Routes :on / :once through `methodName` so both call sites share insert + size bookkeeping.
28
+ local function attach(
29
+ self: Scope,
30
+ source: any,
31
+ fn: (...any) -> (),
32
+ methodName: string
33
+ ): Types.Connection
34
+ local conn = source[methodName](source, fn)
35
+ pushConnection(self, conn)
36
+ return conn
37
+ end
38
+
28
39
  -- Public -----------------------------------------------------------------
29
40
 
30
41
  local ScopeMeta = {}
31
42
  ScopeMeta.__index = ScopeMeta
32
43
 
33
44
  function ScopeMeta.on(self: Scope, source: any, fn: (...any) -> ()): Types.Connection
34
- local conn = source:on(fn)
35
- pushConnection(self, conn)
36
- return conn
45
+ return attach(self, source, fn, "on")
37
46
  end
38
47
 
39
48
  function ScopeMeta.once(self: Scope, source: any, fn: (...any) -> ()): Types.Connection
40
- local conn = source:once(fn)
41
- pushConnection(self, conn)
42
- return conn
49
+ return attach(self, source, fn, "once")
43
50
  end
44
51
 
45
52
  ScopeMeta.add = pushConnection
@@ -48,6 +55,11 @@ function ScopeMeta.size(self: Scope): number
48
55
  return self._count
49
56
  end
50
57
 
58
+ --[[
59
+ Disconnect every tracked source. The shape sniff handles three kinds:
60
+ Roblox RBXScriptConnection (:Disconnect), Lync Connection (:disconnect),
61
+ and tables exposing either capitalization.
62
+ ]]
51
63
  function ScopeMeta.destroy(self: Scope): ()
52
64
  local conns = self._connections
53
65
  local count = self._count
@@ -56,8 +68,12 @@ function ScopeMeta.destroy(self: Scope): ()
56
68
  conns[i] = nil
57
69
  if typeof(conn) == "RBXScriptConnection" then
58
70
  (conn :: RBXScriptConnection):Disconnect()
59
- elseif typeof(conn) == "table" and conn.disconnect then
60
- conn:disconnect()
71
+ elseif typeof(conn) == "table" then
72
+ if conn.disconnect then
73
+ conn:disconnect()
74
+ elseif conn.Disconnect then
75
+ conn:Disconnect()
76
+ end
61
77
  end
62
78
  end
63
79
  self._count = 0
@@ -1,6 +1,6 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- O(1) disconnect signal system with thread reuse and reentrancy-safe fire.
3
+ -- O(1) disconnect signal with thread reuse and reentrancy-safe fire.
4
4
 
5
5
  local Types = require(script.Parent.Parent.Types)
6
6
 
@@ -12,6 +12,7 @@ local STATE_ONCE = 2
12
12
 
13
13
  -- State ------------------------------------------------------------------
14
14
 
15
+ -- Single recyclable thread for async dispatch; saves coroutine.create per fire.
15
16
  local _freeThread: thread? = nil
16
17
 
17
18
  -- Private ----------------------------------------------------------------
@@ -41,14 +42,19 @@ type Entry = {
41
42
  fn: (...any) -> (),
42
43
  _state: number,
43
44
  connected: boolean,
44
- _signalEntries: { Entry },
45
- _signalCount: { number },
45
+ _signal: SignalFields,
46
46
  _index: number,
47
47
  }
48
48
 
49
+ type SignalFields = {
50
+ _entries: { Entry },
51
+ _count: number,
52
+ }
53
+
49
54
  local EntryImpl = {}
50
55
  EntryImpl.__index = EntryImpl
51
56
 
57
+ -- Swap-with-last for O(1) disconnect; the moved entry's _index is patched too.
52
58
  local function disconnectEntry(entry: Entry): ()
53
59
  if entry._state == STATE_DISCONNECTED then
54
60
  return
@@ -57,9 +63,10 @@ local function disconnectEntry(entry: Entry): ()
57
63
  entry._state = STATE_DISCONNECTED
58
64
  entry.connected = false
59
65
 
60
- local entries = entry._signalEntries
61
- local countRef = entry._signalCount
62
- local idx, last = entry._index, countRef[1]
66
+ local signal = entry._signal
67
+ local entries = signal._entries
68
+ local idx = entry._index
69
+ local last = signal._count
63
70
 
64
71
  if idx < last then
65
72
  local moved = entries[last]
@@ -67,31 +74,40 @@ local function disconnectEntry(entry: Entry): ()
67
74
  moved._index = idx
68
75
  end
69
76
  entries[last] = nil
70
- countRef[1] = last - 1
77
+ signal._count = last - 1
71
78
  end
72
79
 
73
80
  EntryImpl.disconnect = disconnectEntry
74
81
  table.freeze(EntryImpl)
75
82
 
76
- type SignalFields = {
77
- _entries: { Entry },
78
- _count: { number },
79
- }
83
+ -- Auto-disconnects ONCE entries before invoking; chooses sync vs spawned thread.
84
+ local function invokeEntry(entry: Entry, sync: boolean, ...: any): ()
85
+ local state = entry._state
86
+ if state == STATE_DISCONNECTED then
87
+ return
88
+ end
89
+ if state == STATE_ONCE then
90
+ disconnectEntry(entry)
91
+ end
92
+ if sync then
93
+ entry.fn(...)
94
+ else
95
+ spawnCallback(entry.fn, ...)
96
+ end
97
+ end
80
98
 
81
99
  local SignalImpl = {}
82
100
  SignalImpl.__index = SignalImpl
83
101
 
84
102
  local function newEntry(self: SignalFields, callback: (...any) -> (), state: number): Entry
85
- local countRef = self._count
86
- local idx = countRef[1] + 1
87
- countRef[1] = idx
103
+ local idx = self._count + 1
104
+ self._count = idx
88
105
 
89
106
  local entry: Entry = setmetatable({
90
107
  fn = callback,
91
108
  _state = state,
92
109
  connected = true,
93
- _signalEntries = self._entries,
94
- _signalCount = countRef,
110
+ _signal = self,
95
111
  _index = idx,
96
112
  }, EntryImpl) :: any
97
113
 
@@ -118,53 +134,28 @@ function SignalImpl.wait(self: SignalFields): ...any
118
134
  end
119
135
 
120
136
  local function dispatch(self: SignalFields, sync: boolean, ...: any): ()
121
- local count = self._count[1]
137
+ local count = self._count
122
138
  if count == 0 then
123
139
  return
124
140
  end
125
141
 
126
142
  local entries = self._entries
127
143
 
144
+ -- Single-listener fast path skips the snapshot allocation.
128
145
  if count == 1 then
129
- local entry = entries[1]
130
- local state = entry._state
131
- if state == STATE_ONCE then
132
- disconnectEntry(entry)
133
- if sync then
134
- entry.fn(...)
135
- else
136
- spawnCallback(entry.fn, ...)
137
- end
138
- elseif state == STATE_CONNECTED then
139
- if sync then
140
- entry.fn(...)
141
- else
142
- spawnCallback(entry.fn, ...)
143
- end
144
- end
146
+ invokeEntry(entries[1], sync, ...)
145
147
  return
146
148
  end
147
149
 
150
+ --[[
151
+ Re-entrancy: a handler may disconnect or add others mid-fire.
152
+ Snapshot the entry list so the iteration view is stable.
153
+ ]]
148
154
  local snapshot = table.create(count)
149
155
  table.move(entries, 1, count, 1, snapshot)
150
156
 
151
157
  for i = 1, count do
152
- local entry = snapshot[i]
153
- local state = entry._state
154
- if state == STATE_ONCE then
155
- disconnectEntry(entry)
156
- if sync then
157
- entry.fn(...)
158
- else
159
- spawnCallback(entry.fn, ...)
160
- end
161
- elseif state == STATE_CONNECTED then
162
- if sync then
163
- entry.fn(...)
164
- else
165
- spawnCallback(entry.fn, ...)
166
- end
167
- end
158
+ invokeEntry(snapshot[i], sync, ...)
168
159
  end
169
160
  end
170
161
 
@@ -177,11 +168,11 @@ function SignalImpl.fireSync(self: SignalFields, ...: any): ()
177
168
  end
178
169
 
179
170
  function SignalImpl.count(self: SignalFields): number
180
- return self._count[1]
171
+ return self._count
181
172
  end
182
173
 
183
174
  function SignalImpl.hasListeners(self: SignalFields): boolean
184
- return self._count[1] > 0
175
+ return self._count > 0
185
176
  end
186
177
 
187
178
  table.freeze(SignalImpl)
@@ -191,7 +182,7 @@ table.freeze(SignalImpl)
191
182
  local SignalModule = {}
192
183
 
193
184
  function SignalModule.create(): Types.SignalLike
194
- return (setmetatable({ _entries = {}, _count = { 0 } }, SignalImpl) :: any) :: Types.SignalLike
185
+ return (setmetatable({ _entries = {}, _count = 0 }, SignalImpl) :: any) :: Types.SignalLike
195
186
  end
196
187
 
197
188
  return table.freeze(SignalModule)
@@ -1,7 +1,8 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Fixed-size codec factory and channel buffer-grow primitive.
3
+ -- Fixed-size codec factory + channel buffer-grow primitive.
4
4
 
5
+ local Log = require(script.Parent.Parent.util.Log)
5
6
  local Types = require(script.Parent.Parent.Types)
6
7
 
7
8
  -- Constants --------------------------------------------------------------
@@ -19,9 +20,9 @@ local countlz = bit32.countlz
19
20
  local lshift = bit32.lshift
20
21
 
21
22
  --[[
22
- Grow `ch.buff` so `ch.cursor + bytes` fits. New size is the next
23
- power of two >= needed, capped by _maxSize. Caller MUST re-read
24
- `ch.buff` after this returns: the buffer reference is replaced.
23
+ Grow `ch.buff` so cursor + bytes fits. New size is the next power of two
24
+ >= needed, capped by _maxSize. Callers MUST re-read `ch.buff` after
25
+ return: the buffer reference is replaced.
25
26
  ]]
26
27
  local function alloc(ch: Types.ChannelState, bytes: number): ()
27
28
  if bytes == 0 then
@@ -36,9 +37,9 @@ local function alloc(ch: Types.ChannelState, bytes: number): ()
36
37
  if needed > _maxSize then
37
38
  local name = ch.currentPacket
38
39
  if name then
39
- error(`[Lync] Channel.alloc("{name}"): buffer overflow ({needed}B, max {_maxSize}B)`)
40
+ Log.error(`packet "{name}" overflowed at {needed}B (max {_maxSize}B)`)
40
41
  end
41
- error(`[Lync] Channel.alloc: buffer overflow ({needed}B, max {_maxSize}B)`)
42
+ Log.error(`overflowed at {needed}B (max {_maxSize}B)`)
42
43
  end
43
44
 
44
45
  local newSize = lshift(1, 32 - countlz(needed - 1))
@@ -53,18 +54,24 @@ end
53
54
  local Base = {}
54
55
 
55
56
  Base.INITIAL_BUF = INITIAL_BUF
56
-
57
57
  Base.alloc = alloc
58
58
 
59
+ -- Floor is INITIAL_BUF: anything smaller errors on the very first write.
59
60
  function Base.setMaxSize(bytes: number): ()
61
+ if bytes < INITIAL_BUF then
62
+ Log.error(`bytes ({bytes}) must be >= {INITIAL_BUF}`)
63
+ end
60
64
  _maxSize = bytes
61
65
  end
62
66
 
67
+ function Base.reset(): ()
68
+ _maxSize = DEFAULT_MAX
69
+ end
70
+
63
71
  --[[
64
- Build a fixed-size codec. The codec exposes channel-level write/read
65
- (which handles buffer growth and cursor bookkeeping) plus the raw
66
- _directWrite / _directRead primitives composite codecs use in their
67
- pre-sized fast paths.
72
+ Build a fixed-size codec. Exposes channel-level write/read (handles
73
+ buffer growth + cursor) plus raw _directWrite/_directRead for composite
74
+ fast paths that pre-size the channel themselves.
68
75
  ]]
69
76
  function Base.define<T>(
70
77
  size: number,
@@ -101,9 +108,9 @@ function Base.define<T>(
101
108
  end
102
109
 
103
110
  --[[
104
- Constant codec: zero bytes wire, decode always returns `value`. Used
105
- by quantizers when min == max so the wire emits nothing yet decode
106
- still produces a type-faithful value.
111
+ Zero-byte codec: writes nothing, reads always return `value`. Used by
112
+ quantizers when min == max so the wire emits nothing yet decode produces
113
+ a type-faithful value.
107
114
  ]]
108
115
  function Base.constant<T>(value: T): Types.InternalCodec<T>
109
116
  return table.freeze({
@@ -1,98 +1,62 @@
1
1
  --!strict
2
2
  --!native
3
- -- Array and deltaArray codecs. Bool elements are bit-packed.
3
+ -- array() and deltaArray(). Bool elements are bit-packed.
4
4
 
5
5
  local Base = require(script.Parent.Parent.Base)
6
- local Channel = require(script.Parent.Parent.Parent.internal.Channel)
7
- local Shared = require(script.Parent.Shared)
6
+ local Baseline = require(script.Parent.Parent.Parent.internal.Baseline)
7
+ local Log = require(script.Parent.Parent.Parent.util.Log)
8
+ local Shared = require(script.Parent.Parent.composite.Shared)
8
9
  local Types = require(script.Parent.Parent.Parent.Types)
9
10
  local Varint = require(script.Parent.Parent.primitive.Varint)
10
11
 
11
12
  -- Private ----------------------------------------------------------------
12
13
 
13
14
  local alloc = Base.alloc
14
- local writeByte = Channel.writeByte
15
- local writeu8 = buffer.writeu8
16
- local readu8 = buffer.readu8
17
- local band = bit32.band
18
- local bor = bit32.bor
19
- local lshift = bit32.lshift
20
-
21
- local acquireScratch = Shared.acquireScratch
22
- local releaseScratch = Shared.releaseScratch
23
- local rangeEqual = Shared.rangeEqual
24
- local snapshot = Shared.snapshot
25
15
  local allocDeltaId = Shared.allocDeltaId
26
- local DELTA_UNCHANGED = Shared.DELTA_UNCHANGED
27
- local DELTA_FULL = Shared.DELTA_FULL
16
+ local boolBytes = Shared.boolBytes
17
+ local packBoolArray = Shared.packBoolArray
18
+ local unpackBoolArray = Shared.unpackBoolArray
19
+ local varintRead = Varint.read
20
+ local varintWrite = Varint.write
28
21
 
22
+ -- Bit-packed bool element fast path: 1 bit per entry instead of 1 byte.
29
23
  local function makeBoolArray(
30
24
  element: Types.InternalCodec<any>,
31
25
  maxCount: number?
32
26
  ): Types.InternalCodec<any>
33
- return table.freeze({
27
+ return {
34
28
  _isArray = true,
35
29
  _element = element,
36
30
  _maxCount = maxCount,
37
31
 
38
32
  write = function(ch: Types.ChannelState, value: { boolean }): ()
39
33
  local len = #value
40
- Varint.write(ch, len)
41
- if len == 0 then
42
- return
43
- end
44
-
45
- local byteCount = (len + 7) // 8
46
- local cursor = ch.cursor
47
- if cursor + byteCount > ch.size then
48
- alloc(ch, byteCount)
49
- end
50
- local b = ch.buff
51
-
52
- local idx = 1
53
- for byteIdx = 0, byteCount - 1 do
54
- local packed = 0
55
- local remaining = len - idx + 1
56
- local limit = if remaining >= 8 then 8 else remaining
57
- for bit = 0, limit - 1 do
58
- if value[idx] then
59
- packed = bor(packed, lshift(1, bit))
60
- end
61
- idx += 1
62
- end
63
- writeu8(b, cursor + byteIdx, packed)
64
- end
65
- ch.cursor = cursor + byteCount
34
+ varintWrite(ch, len)
35
+ packBoolArray(ch, value, len)
66
36
  end,
67
37
 
68
38
  read = function(src: buffer, pos: number, _refs: { Instance }?): ({ boolean }, number)
69
- local len, lenBytes = Varint.read(src, pos)
39
+ local len, lenBytes = varintRead(src, pos)
70
40
  if maxCount and len > maxCount then
71
- error(`[Lync] Array.read: count {len} exceeds max {maxCount}`)
41
+ Log.error(`count {len} exceeds max {maxCount}`)
72
42
  end
73
43
  if len == 0 then
74
44
  return {}, lenBytes
75
45
  end
76
46
 
77
- local byteCount = (len + 7) // 8
47
+ local byteCount = boolBytes(len)
78
48
  local dataStart = pos + lenBytes
79
- local result = table.create(len)
80
-
81
- local idx = 1
82
- for byteIdx = 0, byteCount - 1 do
83
- local byte = readu8(src, dataStart + byteIdx)
84
- local remaining = len - idx + 1
85
- local limit = if remaining >= 8 then 8 else remaining
86
- for bit = 0, limit - 1 do
87
- result[idx] = band(byte, lshift(1, bit)) ~= 0
88
- idx += 1
89
- end
49
+ if dataStart + byteCount > buffer.len(src) then
50
+ Log.error(`payload {byteCount}B exceeds remaining buffer`)
90
51
  end
52
+ local result = table.create(len, false)
53
+ unpackBoolArray(src, dataStart, len, result)
91
54
  return result, lenBytes + byteCount
92
55
  end,
93
- })
56
+ }
94
57
  end
95
58
 
59
+ -- Fixed-size element fast path: single alloc + tight loop calling _direct fns.
96
60
  local function makeDirectArray(
97
61
  element: Types.InternalCodec<any>,
98
62
  elemSize: number,
@@ -100,14 +64,14 @@ local function makeDirectArray(
100
64
  elemDR: (b: buffer, off: number) -> any,
101
65
  maxCount: number?
102
66
  ): Types.InternalCodec<any>
103
- return table.freeze({
67
+ return {
104
68
  _isArray = true,
105
69
  _element = element,
106
70
  _maxCount = maxCount,
107
71
 
108
72
  write = function(ch: Types.ChannelState, value: { any }): ()
109
73
  local len = #value
110
- Varint.write(ch, len)
74
+ varintWrite(ch, len)
111
75
  if len == 0 then
112
76
  return
113
77
  end
@@ -118,7 +82,6 @@ local function makeDirectArray(
118
82
  alloc(ch, payloadBytes)
119
83
  end
120
84
  local b = ch.buff
121
-
122
85
  local off = cursor
123
86
  for i = 1, len do
124
87
  elemDW(b, off, value[i])
@@ -128,9 +91,9 @@ local function makeDirectArray(
128
91
  end,
129
92
 
130
93
  read = function(src: buffer, pos: number, _refs: { Instance }?): ({ any }, number)
131
- local len, lenBytes = Varint.read(src, pos)
94
+ local len, lenBytes = varintRead(src, pos)
132
95
  if maxCount and len > maxCount then
133
- error(`[Lync] Array.read: count {len} exceeds max {maxCount}`)
96
+ Log.error(`count {len} exceeds max {maxCount}`)
134
97
  end
135
98
  if len == 0 then
136
99
  return {}, lenBytes
@@ -138,10 +101,8 @@ local function makeDirectArray(
138
101
 
139
102
  local payloadBytes = len * elemSize
140
103
  local dataStart = pos + lenBytes
141
- local remaining = buffer.len(src) - dataStart
142
- if payloadBytes > remaining then
143
- len = remaining // elemSize
144
- payloadBytes = len * elemSize
104
+ if payloadBytes > buffer.len(src) - dataStart then
105
+ Log.error(`payload {payloadBytes}B exceeds remaining buffer`)
145
106
  end
146
107
 
147
108
  local result = table.create(len)
@@ -152,30 +113,31 @@ local function makeDirectArray(
152
113
  end
153
114
  return result, lenBytes + payloadBytes
154
115
  end,
155
- })
116
+ }
156
117
  end
157
118
 
119
+ -- Generic fallback: variable-size or composite element codec.
158
120
  local function makeGenericArray(
159
121
  element: Types.InternalCodec<any>,
160
122
  maxCount: number?
161
123
  ): Types.InternalCodec<any>
162
- return table.freeze({
124
+ return {
163
125
  _isArray = true,
164
126
  _element = element,
165
127
  _maxCount = maxCount,
166
128
 
167
129
  write = function(ch: Types.ChannelState, value: { any }): ()
168
130
  local len = #value
169
- Varint.write(ch, len)
131
+ varintWrite(ch, len)
170
132
  for i = 1, len do
171
133
  element.write(ch, value[i])
172
134
  end
173
135
  end,
174
136
 
175
137
  read = function(src: buffer, pos: number, refs: { Instance }?): ({ any }, number)
176
- local len, lenBytes = Varint.read(src, pos)
138
+ local len, lenBytes = varintRead(src, pos)
177
139
  if maxCount and len > maxCount then
178
- error(`[Lync] Array.read: count {len} exceeds max {maxCount}`)
140
+ Log.error(`count {len} exceeds max {maxCount}`)
179
141
  end
180
142
  if len == 0 then
181
143
  return {}, lenBytes
@@ -186,14 +148,14 @@ local function makeGenericArray(
186
148
  for i = 1, len do
187
149
  local v, consumed = element.read(src, pos + total, refs)
188
150
  if consumed == 0 then
189
- error(`[Lync] Array.read: element codec consumed 0 bytes at index {i}`)
151
+ Log.error(`element codec consumed 0 bytes at index {i}`)
190
152
  end
191
153
  result[i] = v
192
154
  total += consumed
193
155
  end
194
156
  return result, total
195
157
  end,
196
- })
158
+ }
197
159
  end
198
160
 
199
161
  -- Public -----------------------------------------------------------------
@@ -201,75 +163,35 @@ end
201
163
  local Array = {}
202
164
 
203
165
  function Array.array(element: Types.InternalCodec<any>, maxCount: number?): Types.InternalCodec<any>
166
+ local codec: Types.InternalCodec<any>
204
167
  if element._isBool then
205
- return makeBoolArray(element, maxCount)
168
+ codec = makeBoolArray(element, maxCount)
169
+ else
170
+ local elemSize = element._size
171
+ local elemDW = element._directWrite
172
+ local elemDR = element._directRead
173
+ if elemSize and elemDW and elemDR then
174
+ codec = makeDirectArray(element, elemSize, elemDW, elemDR, maxCount)
175
+ else
176
+ codec = makeGenericArray(element, maxCount)
177
+ end
206
178
  end
207
-
208
- local elemSize = element._size
209
- local elemDW = element._directWrite
210
- local elemDR = element._directRead
211
- if elemSize and elemDW and elemDR then
212
- return makeDirectArray(element, elemSize, elemDW, elemDR, maxCount)
179
+ if element._hasUnknown then
180
+ codec._hasUnknown = true
213
181
  end
214
-
215
- return makeGenericArray(element, maxCount)
182
+ return table.freeze(codec)
216
183
  end
217
184
 
185
+ --[[
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.
189
+ ]]
218
190
  function Array.deltaArray(
219
191
  element: Types.InternalCodec<any>,
220
192
  maxCount: number?
221
193
  ): Types.InternalCodec<any>
222
- local deltaId = allocDeltaId()
223
- local innerArray = Array.array(element, maxCount)
224
-
225
- return table.freeze({
226
- _isDelta = true,
227
-
228
- write = function(ch: Types.ChannelState, value: { any }): ()
229
- local cache = ch.deltas[deltaId]
230
-
231
- if not cache then
232
- writeByte(ch, DELTA_FULL)
233
-
234
- local scratch = acquireScratch()
235
- innerArray.write(scratch, value)
236
- local snapLen = scratch.cursor
237
-
238
- ch.deltas[deltaId] = { raw = snapshot(scratch.buff, snapLen), len = snapLen }
239
- innerArray.write(ch, value)
240
- releaseScratch()
241
- return
242
- end
243
-
244
- local scratch = acquireScratch()
245
- innerArray.write(scratch, value)
246
- local scratchLen = scratch.cursor
247
-
248
- if
249
- scratchLen == cache.len
250
- and rangeEqual(scratch.buff, 0, cache.raw, 0, scratchLen)
251
- then
252
- writeByte(ch, DELTA_UNCHANGED)
253
- releaseScratch()
254
- return
255
- end
256
-
257
- writeByte(ch, DELTA_FULL)
258
- innerArray.write(ch, value)
259
-
260
- cache.raw = snapshot(scratch.buff, scratchLen)
261
- cache.len = scratchLen
262
- releaseScratch()
263
- end,
264
-
265
- read = function(src: buffer, pos: number, refs: { Instance }?): ({ any }, number)
266
- if readu8(src, pos) == DELTA_UNCHANGED then
267
- return {}, 1
268
- end
269
- local value, consumed = innerArray.read(src, pos + 1, refs)
270
- return value, 1 + consumed
271
- end,
272
- })
194
+ return Shared.makeUnchangedOrFullDelta(Array.array(element, maxCount), allocDeltaId(), Baseline)
273
195
  end
274
196
 
275
197
  return table.freeze(Array)