@axpecter/lync 1.5.2 → 2.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.
@@ -47,6 +47,14 @@ function Struct.struct(schema: { [string]: Codec<any> }): Codec<any>
47
47
  local boolByteCount = ceil(boolCount / 8)
48
48
  local hasBools = boolCount > 0
49
49
 
50
+ local hasUnknown = false
51
+ for _, fieldCodec in schema do
52
+ if (fieldCodec :: any)._hasUnknown then
53
+ hasUnknown = true
54
+ break
55
+ end
56
+ end
57
+
50
58
  local canDirect = true
51
59
  local fixedSize = boolByteCount
52
60
  local isAllFixed = true
@@ -126,6 +134,7 @@ function Struct.struct(schema: { [string]: Codec<any> }): Codec<any>
126
134
  _size = fixedSize,
127
135
  _directWrite = directWriteBody,
128
136
  _directRead = directReadBody,
137
+ _hasUnknown = hasUnknown or nil,
129
138
 
130
139
  write = function(ch: ChannelState, value: any): ()
131
140
  local c = ch.cursor
@@ -152,6 +161,7 @@ function Struct.struct(schema: { [string]: Codec<any> }): Codec<any>
152
161
 
153
162
  return table.freeze({
154
163
  _size = if isAllFixed then fixedSize else nil,
164
+ _hasUnknown = hasUnknown or nil,
155
165
 
156
166
  write = function(ch: ChannelState, value: any): ()
157
167
  if isAllFixed then
@@ -195,6 +205,14 @@ function Struct.deltaStruct(schema: { [string]: Codec<any> }): Codec<any>
195
205
  local bitmaskBytes = ceil(segCount / 8)
196
206
  local deltaId = Shared.allocDeltaId()
197
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
+
198
216
  -- Scratch bounds reused every frame(only cloned into cache)
199
217
  local _scratchBounds = table.create(segCount + 1) :: { number }
200
218
  _scratchBounds[1] = 0
@@ -237,6 +255,7 @@ function Struct.deltaStruct(schema: { [string]: Codec<any> }): Codec<any>
237
255
 
238
256
  return table.freeze({
239
257
  _isDelta = true,
258
+ _hasUnknown = hasUnknown or nil,
240
259
 
241
260
  write = function(ch: ChannelState, value: any): ()
242
261
  local cache = ch.deltas[deltaId] :: DeltaCache?
@@ -413,7 +432,10 @@ function Struct.deltaStruct(schema: { [string]: Codec<any> }): Codec<any>
413
432
  end
414
433
 
415
434
  local cache = Baseline.getCache(deltaId) :: any
416
- local result = if cache then table.clone(cache) else {}
435
+ if not cache then
436
+ error("[Lync] Delta frame without baseline")
437
+ end
438
+ local result = table.clone(cache)
417
439
 
418
440
  local maskStart = absPos
419
441
  absPos += bitmaskBytes
@@ -71,6 +71,14 @@ function Tagged.define(tagField: string, variants: { [string]: Codec<any> }): Co
71
71
  end
72
72
  end
73
73
 
74
+ local hasUnknown = false
75
+ for i = 1, count do
76
+ if (tagCodecs[i] :: any)._hasUnknown then
77
+ hasUnknown = true
78
+ break
79
+ end
80
+ end
81
+
74
82
  table.freeze(nameToTag)
75
83
  table.freeze(tagCodecs)
76
84
  table.freeze(tagNames)
@@ -86,6 +94,7 @@ function Tagged.define(tagField: string, variants: { [string]: Codec<any> }): Co
86
94
  if fixedSize then
87
95
  return table.freeze({
88
96
  _size = fixedSize,
97
+ _hasUnknown = hasUnknown or nil,
89
98
  _directWrite = function(b: buffer, offset: number, value: any): ()
90
99
  local tag = nameToTag[value[tagField]]
91
100
  if not tag then
@@ -141,6 +150,7 @@ function Tagged.define(tagField: string, variants: { [string]: Codec<any> }): Co
141
150
 
142
151
  return table.freeze({
143
152
  _size = nil,
153
+ _hasUnknown = hasUnknown or nil,
144
154
  write = function(ch: ChannelState, value: any): ()
145
155
  local tag = nameToTag[value[tagField]]
146
156
  if not tag then
@@ -183,6 +193,7 @@ function Tagged.define(tagField: string, variants: { [string]: Codec<any> }): Co
183
193
 
184
194
  return table.freeze({
185
195
  _size = fixedSize,
196
+ _hasUnknown = hasUnknown or nil,
186
197
  write = function(ch: ChannelState, value: any): ()
187
198
  local tag = nameToTag[value[tagField]]
188
199
  if not tag then
@@ -24,6 +24,14 @@ function Tuple.define(...: Codec<any>): Codec<{ any }>
24
24
 
25
25
  table.freeze(codecs)
26
26
 
27
+ local hasUnknown = false
28
+ for i = 1, count do
29
+ if (codecs[i] :: any)._hasUnknown then
30
+ hasUnknown = true
31
+ break
32
+ end
33
+ end
34
+
27
35
  local canDirect = true
28
36
  local totalSize: number = 0
29
37
  local directWrites = table.create(count) :: { any }
@@ -92,6 +100,7 @@ function Tuple.define(...: Codec<any>): Codec<{ any }>
92
100
  _size = totalSize,
93
101
  _directWrite = directWriteBody,
94
102
  _directRead = directReadBody,
103
+ _hasUnknown = hasUnknown or nil,
95
104
  write = function(ch: ChannelState, value: { any }): ()
96
105
  local c = ch.cursor
97
106
  if c + totalSize > ch.size then
@@ -119,6 +128,7 @@ function Tuple.define(...: Codec<any>): Codec<{ any }>
119
128
 
120
129
  return table.freeze({
121
130
  _size = totalSize,
131
+ _hasUnknown = hasUnknown or nil,
122
132
  write = function(ch: ChannelState, value: { any }): ()
123
133
  if hasFixedTotal then
124
134
  alloc(ch, totalSize :: number)
@@ -41,6 +41,9 @@ InstanceCodec.inst = table.freeze({
41
41
  if not inst then
42
42
  error(`[Lync] Instance ref index out of bounds: {idx}`)
43
43
  end
44
+ if typeof(inst) ~= "Instance" then
45
+ error("[Lync] Invalid ref: expected Instance")
46
+ end
44
47
 
45
48
  return inst, 2
46
49
  end,
@@ -11,6 +11,7 @@ local alloc = Channel.alloc
11
11
 
12
12
  return table.freeze({
13
13
  unknown = table.freeze({
14
+ _hasUnknown = true,
14
15
  write = function(ch: ChannelState, value: any): ()
15
16
  local refs = ch.refs
16
17
  local idx = #refs + 1
package/src/index.d.ts CHANGED
@@ -100,9 +100,9 @@ export type Target = Player | AllTarget | ExceptTarget | GroupObject | Player[];
100
100
 
101
101
  export interface Scope {
102
102
  add(this: Scope, conn: Connection | RBXScriptConnection): void;
103
- listen<T>(this: Scope, source: Packet<T>, callback: (data: T, sender: Player | undefined) => void): void;
104
- once<T>(this: Scope, source: Packet<T>, callback: (data: T, sender: Player | undefined) => void): void;
105
- listenAll(this: Scope, namespace: Namespace, callback: (name: string, data: unknown, sender: Player | undefined) => void): void;
103
+ listen<T>(this: Scope, source: Packet<T>, callback: (data: T, sender: Player | undefined, timestamp: number | undefined) => void): void;
104
+ once<T>(this: Scope, source: Packet<T>, callback: (data: T, sender: Player | undefined, timestamp: number | undefined) => void): void;
105
+ listenAll(this: Scope, namespace: Namespace, callback: (name: string, data: unknown, sender: Player | undefined, timestamp: number | undefined) => void): void;
106
106
  destroy(this: Scope): void;
107
107
  }
108
108
 
@@ -120,14 +120,26 @@ export interface PacketConfig<T> {
120
120
  rateLimit?: RateLimitConfig;
121
121
  validate?: (data: T, player: Player) => LuaTuple<[boolean, string?]>;
122
122
  maxPayloadBytes?: number;
123
+ timestamp?: "frame" | "offset" | "full";
124
+ }
125
+
126
+ export interface PlayerStats {
127
+ bytesSent: number;
128
+ bytesReceived: number;
123
129
  }
124
130
 
125
131
  export interface Packet<T> {
126
132
  send(this: Packet<T>, data: T, target?: Target): void;
127
- listen(this: Packet<T>, callback: (data: T, sender: Player | undefined) => void): Connection;
128
- once(this: Packet<T>, callback: (data: T, sender: Player | undefined) => void): Connection;
133
+ listen(this: Packet<T>, callback: (data: T, sender: Player | undefined, timestamp: number | undefined) => void): Connection;
134
+ once(this: Packet<T>, callback: (data: T, sender: Player | undefined, timestamp: number | undefined) => void): Connection;
129
135
  wait(this: Packet<T>): LuaTuple<[T, Player | undefined]>;
130
136
  disconnectAll(this: Packet<T>): void;
137
+
138
+ getBytesSent(this: Packet<T>): number;
139
+ getBytesReceived(this: Packet<T>): number;
140
+ getFires(this: Packet<T>): number;
141
+ getRecvFires(this: Packet<T>): number;
142
+ getDrops(this: Packet<T>): number;
131
143
  }
132
144
 
133
145
  // -- Query -------------------------------------------------------------
@@ -182,7 +194,7 @@ export interface Namespace {
182
194
  readonly queries: Record<string, Query<unknown, unknown>>;
183
195
  listenAll(
184
196
  this: Namespace,
185
- callback: (name: string, data: unknown, sender: Player | undefined) => void,
197
+ callback: (name: string, data: unknown, sender: Player | undefined, timestamp: number | undefined) => void,
186
198
  ): Connection;
187
199
  onSend(
188
200
  this: Namespace,
@@ -306,13 +318,30 @@ declare namespace Lync {
306
318
  // Scope
307
319
  export function scope(): Scope;
308
320
 
309
- // Configuration
321
+ // Configuration (call before start)
310
322
  export function setChannelMaxSize(bytes: number): void;
311
323
  export function setValidationDepth(depth: number): void;
312
324
  export function setPoolSize(count: number): void;
313
325
 
326
+ // Runtime configuration
327
+ export function flush(): void;
328
+ export function setFlushRate(hz: number): void;
329
+
330
+ // Stats (default off; call enableStats() to activate)
331
+ export function enableStats(): void;
332
+ export function getPlayerStats(player: Player): PlayerStats | undefined;
333
+ export function resetStats(): void;
334
+
335
+ // Security (server-only)
336
+ export function setBandwidthLimit(softLimit: number, maxStrikes: number): void;
337
+
314
338
  // Introspection
315
339
  export function queryPendingCount(): number;
340
+
341
+ // Packet capture (server-only, debug)
342
+ export function startCapture(label?: string): void;
343
+ export function stopCapture(): void;
344
+ export function dumpCaptures(): void;
316
345
  }
317
346
 
318
347
  export default Lync;
package/src/init.luau CHANGED
@@ -103,8 +103,38 @@ end
103
103
 
104
104
  -- Public --------------------------------------------------------------
105
105
 
106
+ local IS_SERVER = RunService:IsServer()
107
+
108
+ local function flush(): ()
109
+ if IS_SERVER then
110
+ Server.flush()
111
+ else
112
+ Client.flush()
113
+ end
114
+ end
115
+
116
+ local function setFlushRate(hz: number): ()
117
+ if IS_SERVER then
118
+ Server.setFlushRate(hz)
119
+ else
120
+ Client.setFlushRate(hz)
121
+ end
122
+ end
123
+
124
+ local function getPlayerStats(player: Player): any?
125
+ return Server.getPlayerStats(player)
126
+ end
127
+
128
+ local function resetStats(): ()
129
+ Server.resetStats()
130
+ end
131
+
132
+ local function setBandwidthLimit(softLimit: number, maxStrikes: number): ()
133
+ Server.setBandwidthLimit(softLimit, maxStrikes)
134
+ end
135
+
106
136
  local Lync = {
107
- VERSION = "1.5.2",
137
+ VERSION = "2.0.0",
108
138
 
109
139
  -- Lifecycle
110
140
  start = start,
@@ -187,13 +217,30 @@ local Lync = {
187
217
  -- Scope
188
218
  scope = ScopeModule.create,
189
219
 
190
- -- Configuration(call before start)
220
+ -- Configuration (call before start)
191
221
  setChannelMaxSize = Channel.setMaxSize,
192
222
  setValidationDepth = Gate.setMaxDepth,
193
223
  setPoolSize = Pool.setMaxSize,
194
224
 
225
+ -- Runtime configuration
226
+ flush = flush,
227
+ setFlushRate = setFlushRate,
228
+
229
+ -- Stats (default off; call enableStats() to activate)
230
+ enableStats = Channel.enableStats,
231
+ getPlayerStats = getPlayerStats,
232
+ resetStats = resetStats,
233
+
234
+ -- Security (server-only)
235
+ setBandwidthLimit = setBandwidthLimit,
236
+
195
237
  -- Introspection
196
238
  queryPendingCount = QueryModule.pendingCount,
239
+
240
+ -- Packet capture (server-only, debug)
241
+ startCapture = if IS_SERVER then Server.startCapture else nil,
242
+ stopCapture = if IS_SERVER then Server.stopCapture else nil,
243
+ dumpCaptures = if IS_SERVER then Server.dumpCaptures else nil,
197
244
  }
198
245
 
199
246
  Lync.default = Lync
@@ -17,23 +17,35 @@ local bxor = bit32.bxor
17
17
  local band32 = bit32.band
18
18
  local countlz = bit32.countlz
19
19
  local lshift = bit32.lshift
20
+ local bor = bit32.bor
21
+ local rshift = bit32.rshift
22
+
23
+ local TS_NONE = 0
24
+ local TS_FRAME = 1
25
+ local TS_OFFSET = 2
26
+ local TS_FULL = 3
20
27
 
21
28
  -- State ---------------------------------------------------------------
22
29
 
23
30
  local _maxSize = MAX_SIZE
24
-
25
- -- Packet name for overflow diagnostics. Not coroutine-safe: concurrent
26
- -- serializers may overwrite before the error fires.
27
31
  local _currentPacket = ""
28
32
 
33
+ -- Timestamp state updated once per flush via updateTimestamp().
34
+ local _frameCounter: number = 0
35
+ local _offsetMs: number = 0
36
+ local _clockTime: number = 0
37
+ local _anyTimestamp = false
38
+
39
+ -- Stats gating. Default off; set via Lync.enableStats().
40
+ local _statsEnabled = false
41
+
29
42
  -- Public --------------------------------------------------------------
30
43
 
31
44
  local Channel = {}
32
45
 
33
- -- Override the maximum buffer size. Call before start(). Range: 4096–1048576.
34
46
  function Channel.setMaxSize(bytes: number): ()
35
47
  if bytes < 4096 or bytes > 1048576 then
36
- error(`[Lync] Channel max size must be 40961048576, got {bytes}`)
48
+ error(`[Lync] Channel max size must be 4096-1048576, got {bytes}`)
37
49
  end
38
50
  _maxSize = bytes
39
51
  end
@@ -56,7 +68,6 @@ function Channel.create(): ChannelState
56
68
  }
57
69
  end
58
70
 
59
- -- Grows the channel buffer to fit the requested bytes. Power-of-two sizing.
60
71
  function Channel.alloc(ch: ChannelState, bytes: number): ()
61
72
  local needed = ch.cursor + bytes
62
73
  if needed <= ch.size then
@@ -95,52 +106,126 @@ function Channel.sealAndDump(ch: ChannelState): (buffer, { Instance })
95
106
  return out, if #refs > 0 then table.clone(refs) else {}
96
107
  end
97
108
 
98
- -- Sets the packet name for overflow diagnostics.
99
109
  function Channel.setCurrentPacket(name: string): ()
100
110
  _currentPacket = name
101
111
  end
102
112
 
103
- -- Writes a single value into the batch. Opens a new header on ID change or overflow.
104
- function Channel.writeBatch(ch: ChannelState, id: number, name: string, codec: any, data: any): ()
105
- if id ~= ch.lastId or ch.itemCount >= MAX_BATCH then
106
- if ch.lastId >= 0 then
107
- buffer.writeu16(ch.buff, ch.countPos, ch.itemCount)
108
- end
113
+ function Channel.updateTimestamp(frameCounter: number, offsetMs: number, clockTime: number): ()
114
+ _frameCounter = frameCounter
115
+ _offsetMs = offsetMs
116
+ _clockTime = clockTime
117
+ end
109
118
 
110
- _currentPacket = name
111
- alloc(ch, 3)
112
- local c = ch.cursor
113
- buffer.writeu8(ch.buff, c, id)
114
- ch.countPos = c + 1
115
- ch.cursor = c + 3
116
- ch.lastId = id
117
- ch.itemCount = 0
119
+ function Channel.enableTimestamps(): ()
120
+ _anyTimestamp = true
121
+ end
122
+
123
+ function Channel.hasTimestamps(): boolean
124
+ return _anyTimestamp
125
+ end
126
+
127
+ function Channel.enableStats(): ()
128
+ _statsEnabled = true
129
+ end
130
+
131
+ function Channel.statsEnabled(): boolean
132
+ return _statsEnabled
133
+ end
134
+
135
+ -- Batch opener: no timestamp variant. Resolved at define time via reg._openFn (#2)
136
+ local function openBatchPlain(ch: ChannelState, id: number, name: string): ()
137
+ if ch.lastId >= 0 then
138
+ buffer.writeu16(ch.buff, ch.countPos, ch.itemCount)
139
+ end
140
+ _currentPacket = name
141
+ alloc(ch, 3)
142
+ local c = ch.cursor
143
+ buffer.writeu8(ch.buff, c, id)
144
+ ch.countPos = c + 1
145
+ ch.cursor = c + 3
146
+ ch.lastId = id
147
+ ch.itemCount = 0
148
+ end
149
+
150
+ local function openBatchFrame(ch: ChannelState, id: number, name: string): ()
151
+ if ch.lastId >= 0 then
152
+ buffer.writeu16(ch.buff, ch.countPos, ch.itemCount)
153
+ end
154
+ _currentPacket = name
155
+ alloc(ch, 4)
156
+ local c = ch.cursor
157
+ buffer.writeu8(ch.buff, c, id)
158
+ ch.countPos = c + 1
159
+ buffer.writeu8(ch.buff, c + 3, _frameCounter)
160
+ ch.cursor = c + 4
161
+ ch.lastId = id
162
+ ch.itemCount = 0
163
+ end
164
+
165
+ local function openBatchOffset(ch: ChannelState, id: number, name: string): ()
166
+ if ch.lastId >= 0 then
167
+ buffer.writeu16(ch.buff, ch.countPos, ch.itemCount)
168
+ end
169
+ _currentPacket = name
170
+ alloc(ch, 5)
171
+ local c = ch.cursor
172
+ buffer.writeu8(ch.buff, c, id)
173
+ ch.countPos = c + 1
174
+ buffer.writeu16(ch.buff, c + 3, _offsetMs)
175
+ ch.cursor = c + 5
176
+ ch.lastId = id
177
+ ch.itemCount = 0
178
+ end
179
+
180
+ local function openBatchFull(ch: ChannelState, id: number, name: string): ()
181
+ if ch.lastId >= 0 then
182
+ buffer.writeu16(ch.buff, ch.countPos, ch.itemCount)
183
+ end
184
+ _currentPacket = name
185
+ alloc(ch, 11)
186
+ local c = ch.cursor
187
+ buffer.writeu8(ch.buff, c, id)
188
+ ch.countPos = c + 1
189
+ buffer.writef64(ch.buff, c + 3, _clockTime)
190
+ ch.cursor = c + 11
191
+ ch.lastId = id
192
+ ch.itemCount = 0
193
+ end
194
+
195
+ -- Returns the correct openBatch function for a given timestamp mode.
196
+ function Channel.resolveOpenFn(tsMode: number): (ch: ChannelState, id: number, name: string) -> ()
197
+ if tsMode == TS_FRAME then
198
+ return openBatchFrame
199
+ elseif tsMode == TS_OFFSET then
200
+ return openBatchOffset
201
+ elseif tsMode == TS_FULL then
202
+ return openBatchFull
203
+ end
204
+ return openBatchPlain
205
+ end
206
+
207
+ -- Writes a single value into the batch. Zero branching on the hot path.
208
+ function Channel.writeBatch(ch: ChannelState, reg: any, data: any): ()
209
+ local id = reg.id
210
+ if id ~= ch.lastId or ch.itemCount >= MAX_BATCH then
211
+ reg._openFn(ch, id, reg.name)
118
212
  end
119
213
 
120
214
  ch.itemCount += 1
121
- codec.write(ch, data)
215
+ reg.codec.write(ch, data)
122
216
  end
123
217
 
124
218
  -- Appends pre-serialized bytes into a batch. Used by broadcast fan-out.
125
219
  function Channel.writeBatchRaw(
126
220
  ch: ChannelState,
127
- id: number,
221
+ reg: any,
128
222
  src: buffer,
129
223
  srcLen: number,
130
224
  srcRefs: { Instance }?
131
225
  ): ()
226
+ local id = reg.id
132
227
  if id ~= ch.lastId or ch.itemCount >= MAX_BATCH then
133
- if ch.lastId >= 0 then
134
- buffer.writeu16(ch.buff, ch.countPos, ch.itemCount)
135
- end
136
-
137
- alloc(ch, 3)
138
- local c = ch.cursor
139
- buffer.writeu8(ch.buff, c, id)
140
- ch.countPos = c + 1
141
- ch.cursor = c + 3
142
- ch.lastId = id
143
- ch.itemCount = 0
228
+ reg._openFn(ch, id, reg.name)
144
229
  end
145
230
 
146
231
  ch.itemCount += 1
@@ -168,7 +253,7 @@ end
168
253
 
169
254
  --[[
170
255
  Writes a query frame. Seals any open batch, then writes:
171
- id(u8) + correlationId(u16) + status(u8)
256
+ id(u8) + correlationId(varint) + status(u8)
172
257
  Status 0 = payload follows. Status 1 = nil response.
173
258
  ]]
174
259
  function Channel.writeQuery(
@@ -182,18 +267,39 @@ function Channel.writeQuery(
182
267
  _currentPacket = name
183
268
  sealOpen(ch)
184
269
 
185
- alloc(ch, 4)
270
+ alloc(ch, 2)
186
271
  local c = ch.cursor
187
272
  buffer.writeu8(ch.buff, c, id)
188
- buffer.writeu16(ch.buff, c + 1, correlationId)
273
+ ch.cursor = c + 1
274
+
275
+ -- Inline varint write to avoid circular dependency (Varint requires Channel)
276
+ local v = correlationId
277
+ if v < 0x80 then
278
+ alloc(ch, 1)
279
+ buffer.writeu8(ch.buff, ch.cursor, v)
280
+ ch.cursor += 1
281
+ else
282
+ alloc(ch, 5)
283
+ local b = ch.buff
284
+ local vc = ch.cursor
285
+ while v >= 0x80 do
286
+ buffer.writeu8(b, vc, bor(band32(v, 0x7F), 0x80))
287
+ v = rshift(v, 7)
288
+ vc += 1
289
+ end
290
+ buffer.writeu8(b, vc, v)
291
+ ch.cursor = vc + 1
292
+ end
189
293
 
294
+ local c2 = ch.cursor
295
+ alloc(ch, 1)
190
296
  if codec then
191
- buffer.writeu8(ch.buff, c + 3, 0)
192
- ch.cursor = c + 4
297
+ buffer.writeu8(ch.buff, c2, 0)
298
+ ch.cursor = c2 + 1
193
299
  codec.write(ch, data)
194
300
  else
195
- buffer.writeu8(ch.buff, c + 3, 1)
196
- ch.cursor = c + 4
301
+ buffer.writeu8(ch.buff, c2, 1)
302
+ ch.cursor = c2 + 1
197
303
  end
198
304
  end
199
305
 
@@ -229,12 +335,36 @@ function Channel.xorApply(current: buffer, previous: buffer?): buffer
229
335
  return result
230
336
  end
231
337
 
338
+ -- Conditional refs clear (#6)
232
339
  function Channel.reset(ch: ChannelState): ()
233
340
  ch.cursor = 0
234
341
  ch.lastId = -1
235
342
  ch.countPos = 0
236
343
  ch.itemCount = 0
237
- table.clear(ch.refs)
344
+ if #ch.refs > 0 then
345
+ table.clear(ch.refs)
346
+ end
347
+ end
348
+
349
+ -- Test helper: builds a minimal reg-like object for writeBatch.
350
+ function Channel.mockReg(id: number, name: string, codec: any): any
351
+ return {
352
+ id = id,
353
+ name = name,
354
+ codec = codec,
355
+ timestampMode = 0,
356
+ _openFn = openBatchPlain,
357
+ drops = 0,
358
+ bytesSent = 0,
359
+ bytesReceived = 0,
360
+ fires = 0,
361
+ recvFires = 0,
362
+ }
238
363
  end
239
364
 
365
+ Channel.TS_NONE = TS_NONE
366
+ Channel.TS_FRAME = TS_FRAME
367
+ Channel.TS_OFFSET = TS_OFFSET
368
+ Channel.TS_FULL = TS_FULL
369
+
240
370
  return table.freeze(Channel)
@@ -2,6 +2,7 @@
2
2
  --!optimize 2
3
3
  -- Deterministic ID assignment for packets and queries.
4
4
 
5
+ local Channel = require(script.Parent.Channel)
5
6
  local Types = require(script.Parent.Parent.Types)
6
7
 
7
8
  type Codec<T> = Types.Codec<T>
@@ -32,7 +33,8 @@ local function assign(
32
33
  signal: any,
33
34
  rateLimit: RateLimitConfig?,
34
35
  validate: ((data: any, player: Player) -> (boolean, string?))?,
35
- maxPayloadBytes: number?
36
+ maxPayloadBytes: number?,
37
+ timestampMode: number?
36
38
  ): Registration
37
39
  if _names[name] then
38
40
  error(`[Lync] Duplicate registration: "{name}"`)
@@ -70,6 +72,13 @@ local function assign(
70
72
  validate = validate,
71
73
  needsGate = rateLimit ~= nil or validate ~= nil,
72
74
  maxPayloadBytes = maxPayloadBytes,
75
+ timestampMode = timestampMode or 0,
76
+ _openFn = Channel.resolveOpenFn(timestampMode or 0),
77
+ bytesSent = 0,
78
+ bytesReceived = 0,
79
+ fires = 0,
80
+ recvFires = 0,
81
+ drops = 0,
73
82
  }
74
83
 
75
84
  _entries[id] = reg
@@ -92,7 +101,8 @@ function Registry.register(
92
101
  signal: any,
93
102
  rateLimit: RateLimitConfig?,
94
103
  validate: ((data: any, player: Player) -> (boolean, string?))?,
95
- maxPayloadBytes: number?
104
+ maxPayloadBytes: number?,
105
+ timestampMode: number?
96
106
  ): Registration
97
107
  return assign(
98
108
  name,
@@ -103,7 +113,8 @@ function Registry.register(
103
113
  signal,
104
114
  rateLimit,
105
115
  validate,
106
- maxPayloadBytes
116
+ maxPayloadBytes,
117
+ timestampMode
107
118
  )
108
119
  end
109
120
 
@@ -143,4 +154,10 @@ function Registry.getByName(name: string): Registration?
143
154
  return if id then _entries[id] else nil
144
155
  end
145
156
 
157
+ function Registry.forEach(fn: (reg: Registration) -> ()): ()
158
+ for _, reg in _entries do
159
+ fn(reg)
160
+ end
161
+ end
162
+
146
163
  return table.freeze(Registry)