@axpecter/lync 2.3.2 → 3.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.
Files changed (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +302 -299
  3. package/Types.luau +215 -0
  4. package/api/Define.luau +154 -0
  5. package/api/Lifecycle.luau +119 -0
  6. package/api/Packet.luau +108 -0
  7. package/api/Query.luau +149 -0
  8. package/api/Set.luau +189 -0
  9. package/channel/Blocks.luau +124 -0
  10. package/channel/Group.luau +118 -0
  11. package/channel/Link.luau +135 -0
  12. package/channel/Outbox.luau +261 -0
  13. package/channel/Recipients.luau +129 -0
  14. package/channel/Transport.luau +138 -0
  15. package/codec/Base.luau +236 -0
  16. package/codec/Composites.luau +128 -0
  17. package/codec/Datatypes.luau +162 -0
  18. package/codec/Legality.luau +61 -0
  19. package/codec/Scalars.luau +72 -0
  20. package/codec/Text.luau +89 -0
  21. package/codec/init.luau +39 -0
  22. package/index.d.ts +285 -0
  23. package/init.luau +91 -0
  24. package/kernel/Bits.luau +16 -0
  25. package/kernel/Catalog.luau +121 -0
  26. package/kernel/Heap.luau +80 -0
  27. package/kernel/Listeners.luau +95 -0
  28. package/kernel/Log.luau +197 -0
  29. package/kernel/Tally.luau +128 -0
  30. package/kernel/Threads.luau +44 -0
  31. package/kernel/Wide.luau +267 -0
  32. package/package.json +15 -32
  33. package/runtime/Boundary.luau +180 -0
  34. package/runtime/Dispatch.luau +91 -0
  35. package/runtime/Handles.luau +114 -0
  36. package/runtime/Handshake.luau +90 -0
  37. package/runtime/Pending.luau +196 -0
  38. package/runtime/Router.luau +379 -0
  39. package/runtime/Session.luau +731 -0
  40. package/shape/Compile.luau +340 -0
  41. package/shape/Folds.luau +332 -0
  42. package/shape/Frames.luau +170 -0
  43. package/shape/Grid.luau +169 -0
  44. package/shape/Ir.luau +540 -0
  45. package/shape/Joins.luau +125 -0
  46. package/shape/Legality.luau +273 -0
  47. package/shape/Lower.luau +419 -0
  48. package/shape/Order.luau +64 -0
  49. package/shape/Regions.luau +113 -0
  50. package/shape/Rotation.luau +472 -0
  51. package/shape/Serial.luau +258 -0
  52. package/shape/Words.luau +89 -0
  53. package/shape/emit/Block.luau +453 -0
  54. package/shape/emit/Column.luau +338 -0
  55. package/shape/emit/Leaf.luau +462 -0
  56. package/shape/emit/Word.luau +234 -0
  57. package/shape/emit/init.luau +222 -0
  58. package/shape/init.luau +78 -0
  59. package/state/Apply.luau +330 -0
  60. package/state/Audience.luau +126 -0
  61. package/state/Catchup.luau +218 -0
  62. package/state/Coalesce.luau +43 -0
  63. package/state/Fieldset.luau +177 -0
  64. package/state/Newest.luau +339 -0
  65. package/state/Plan.luau +441 -0
  66. package/state/Sections.luau +468 -0
  67. package/state/Store.luau +188 -0
  68. package/state/Sync.luau +762 -0
  69. package/wire/Bytes.luau +118 -0
  70. package/wire/Clock.luau +37 -0
  71. package/wire/Protocol.luau +110 -0
  72. package/wire/Sequence.luau +40 -0
  73. package/wire/Varint.luau +124 -0
  74. package/src/Types.luau +0 -180
  75. package/src/api/Group.luau +0 -156
  76. package/src/api/Packet.luau +0 -319
  77. package/src/api/Query.luau +0 -338
  78. package/src/api/Scope.luau +0 -90
  79. package/src/api/Signal.luau +0 -188
  80. package/src/codec/Base.luau +0 -136
  81. package/src/codec/composite/Array.luau +0 -456
  82. package/src/codec/composite/Map.luau +0 -469
  83. package/src/codec/composite/Optional.luau +0 -54
  84. package/src/codec/composite/Shared.luau +0 -291
  85. package/src/codec/composite/Struct.luau +0 -494
  86. package/src/codec/composite/Tagged.luau +0 -103
  87. package/src/codec/composite/Tuple.luau +0 -143
  88. package/src/codec/datatype/Buffer.luau +0 -66
  89. package/src/codec/datatype/CFrame.luau +0 -109
  90. package/src/codec/datatype/Color.luau +0 -32
  91. package/src/codec/datatype/Instance.luau +0 -52
  92. package/src/codec/datatype/IntVector.luau +0 -39
  93. package/src/codec/datatype/NumberRange.luau +0 -39
  94. package/src/codec/datatype/Ray.luau +0 -36
  95. package/src/codec/datatype/Rect.luau +0 -31
  96. package/src/codec/datatype/Region.luau +0 -62
  97. package/src/codec/datatype/Sequence.luau +0 -184
  98. package/src/codec/datatype/String.luau +0 -94
  99. package/src/codec/datatype/UDim.luau +0 -46
  100. package/src/codec/datatype/Vector.luau +0 -130
  101. package/src/codec/meta/Auto.luau +0 -318
  102. package/src/codec/meta/Bitfield.luau +0 -147
  103. package/src/codec/meta/Custom.luau +0 -33
  104. package/src/codec/meta/DeltaScalar.luau +0 -390
  105. package/src/codec/meta/Enum.luau +0 -50
  106. package/src/codec/meta/Float.luau +0 -38
  107. package/src/codec/meta/Nothing.luau +0 -13
  108. package/src/codec/meta/Unknown.luau +0 -41
  109. package/src/codec/primitive/Bool.luau +0 -26
  110. package/src/codec/primitive/Float16.luau +0 -99
  111. package/src/codec/primitive/Int.luau +0 -59
  112. package/src/codec/primitive/Number.luau +0 -24
  113. package/src/codec/primitive/Signed.luau +0 -32
  114. package/src/codec/primitive/Varint.luau +0 -165
  115. package/src/codec/primitive/Zint.luau +0 -99
  116. package/src/index.d.ts +0 -423
  117. package/src/init.luau +0 -362
  118. package/src/internal/Baseline.luau +0 -57
  119. package/src/internal/Channel.luau +0 -390
  120. package/src/internal/Middleware.luau +0 -100
  121. package/src/internal/Pool.luau +0 -65
  122. package/src/internal/Registry.luau +0 -119
  123. package/src/internal/Transport.luau +0 -28
  124. package/src/transport/Bridge.luau +0 -110
  125. package/src/transport/Client.luau +0 -97
  126. package/src/transport/Gate.luau +0 -488
  127. package/src/transport/Reader.luau +0 -331
  128. package/src/transport/Server.luau +0 -211
  129. package/src/util/Array.luau +0 -18
  130. package/src/util/Buffer.luau +0 -90
  131. package/src/util/Constants.luau +0 -30
  132. package/src/util/Log.luau +0 -68
  133. package/src/util/Player.luau +0 -14
  134. package/src/util/Quantize.luau +0 -84
  135. package/src/util/Quat.luau +0 -124
package/api/Query.luau ADDED
@@ -0,0 +1,149 @@
1
+ --!strict
2
+ --!optimize 2
3
+ -- The query handle: one responder a side, and a request that answers rather than throws or hangs.
4
+
5
+ local Base = require (script.Parent.Parent.codec.Base)
6
+ local Define = require (script.Parent.Define)
7
+ local Handles = require (script.Parent.Parent.runtime.Handles)
8
+ local Serial = require (script.Parent.Parent.shape.Serial)
9
+ local Types = require (script.Parent.Parent.Types)
10
+
11
+ local Query = {}
12
+ Query.__index = Query
13
+
14
+ -- Private -------------------------------------------------------------
15
+
16
+ -- The one responder this side gets, held until there is a dispatch to hand it to.
17
+ local function answers (self: any, said: string, fn: (...any) -> any)
18
+ local live = self.live
19
+ local under = Define.naming (live, said)
20
+
21
+ local session = live.session
22
+ if session ~= nil and session.started then
23
+ Define.refuse (`{said} after {under} started, and start seals responders`, under)
24
+ end
25
+
26
+ if self.answering ~= nil then
27
+ Define.refuse (`{under} already has a responder, and a query has one`, under)
28
+ end
29
+
30
+ self.answering = fn
31
+ Handles.waiting (live, function (session) session:respond (live.id :: number, under, fn) end)
32
+ end
33
+
34
+ --[[
35
+ A client's request, which yields until it is answered or told why it will not be. A third
36
+ argument is the server form's completion and the client form has no third, so a call carrying
37
+ one is the server's made on the wrong machine. The lookup below cannot refuse, the caller
38
+ having branched on the side it tests, and what it is called for is the session.
39
+ ]]
40
+ local function asked (
41
+ self: any,
42
+ value: any,
43
+ timeout: number?,
44
+ complete: any
45
+ ): (boolean, any, Types.OutcomeData?)
46
+ local session = Define.asking (self.live, "request")
47
+ if complete ~= nil then
48
+ Define.refuse (
49
+ "request with a completion is the server's call, and this is a client",
50
+ Define.naming (self.live, "request")
51
+ )
52
+ end
53
+
54
+ local settled = false
55
+ local parked: thread? = nil
56
+
57
+ local ok: boolean
58
+ local result: any
59
+ local data: Types.OutcomeData?
60
+
61
+ session:ask (self.live.def, nil, value, timeout, function (answered, gave, about)
62
+ ok, result, data = answered, gave, about
63
+ settled = true
64
+
65
+ local held = parked
66
+ if held ~= nil then task.spawn (held) end
67
+ end)
68
+
69
+ if not settled then
70
+ parked = coroutine.running ()
71
+ coroutine.yield ()
72
+ end
73
+
74
+ return ok, result, data
75
+ end
76
+
77
+ -- Public --------------------------------------------------------------
78
+
79
+ --[[
80
+ Two builds and one handle, because a request and its reply are one agreement between two
81
+ machines, and they share one definition and two consecutive ids.
82
+ ]]
83
+ function Query.new <Q, R>(of: Types.Codec<Q>, reply: Types.Codec<R>): Types.Query<Q, R>
84
+ --[[
85
+ Level two is the caller rather than this frame, so a schema refused at start points at the
86
+ declaration instead of the start that caught it.
87
+ ]]
88
+ local file, line = debug.info (2, "sl")
89
+ local self = {
90
+ kind = "query" :: "query",
91
+ of = of,
92
+ reply = reply,
93
+ at = { file = file, line = line },
94
+ live = Handles.live (),
95
+ answering = nil :: ((...any) -> any)?,
96
+ }
97
+
98
+ return (setmetatable (self, Query) :: any) :: Types.Query<Q, R>
99
+ end
100
+
101
+ --[[
102
+ The responder, on whichever side is answering.
103
+
104
+ Two names for one registration, because what a responder is handed differs between the sides
105
+ and what it is registered as does not.
106
+ ]]
107
+ function Query.onServer (self: any, fn: (any, Player) -> any) answers (self, "onServer", fn) end
108
+
109
+ function Query.onClient (self: any, fn: (any) -> any) answers (self, "onClient", fn) end
110
+
111
+ --[[
112
+ One request, in whichever of its two forms this machine has. Which form comes off the machine
113
+ and never off the arguments, so a call before start names the namespace that has not opened
114
+ rather than an argument. What the arguments say afterwards is whether the caller made the other
115
+ side's call, which is the one mistake here that used to answer with somebody else's error.
116
+ ]]
117
+ function Query.request (self: any, first: any, second: any, third: any, fourth: any): ...any
118
+ if self.live.isServer then
119
+ --[[
120
+ The server form carries a completion and the client form has no third argument, so a
121
+ call reaching here without one is the client's. The lookup below cannot refuse, the
122
+ branch above having settled the side before it is asked, and what it is called for is
123
+ the session and the namespace it names when there is none.
124
+ ]]
125
+ local session = Define.serving (self.live, "request")
126
+ if typeof (third) ~= "function" then
127
+ Define.refuse (
128
+ "request without a completion is a client's call, and this is the server",
129
+ Define.naming (self.live, "request")
130
+ )
131
+ end
132
+
133
+ session:ask (self.live.def, first, second, fourth, third)
134
+ return
135
+ end
136
+
137
+ return asked (self, first, second, third)
138
+ end
139
+
140
+ -- The schema as text, which is the wording a refused request is answered with.
141
+ function Query.describe (self: any): string
142
+ return Serial.frame ({
143
+ kind = "query",
144
+ of = { cell = Base.cell (self.of) },
145
+ reply = { cell = Base.cell (self.reply) },
146
+ })
147
+ end
148
+
149
+ return table.freeze (Query)
package/api/Set.luau ADDED
@@ -0,0 +1,189 @@
1
+ --!strict
2
+ --!optimize 2
3
+ -- The set handle: the server's records, the client's view of them, and the one key that splits.
4
+
5
+ local Base = require (script.Parent.Parent.codec.Base)
6
+ local Define = require (script.Parent.Define)
7
+ local Handles = require (script.Parent.Parent.runtime.Handles)
8
+ local Serial = require (script.Parent.Parent.shape.Serial)
9
+ local Sync = require (script.Parent.Parent.state.Sync)
10
+ local Types = require (script.Parent.Parent.Types)
11
+
12
+ local Set = {}
13
+ Set.__index = Set
14
+
15
+ -- Private -------------------------------------------------------------
16
+
17
+ -- The set this handle mutates, which only the server has records in.
18
+ local function owning (self: any, said: string): Sync.Sync
19
+ local live = self.live
20
+ return Define.serving (live, said):set (live.id :: number)
21
+ end
22
+
23
+ --[[
24
+ The set this handle reads, or nothing where its namespace has not opened one.
25
+
26
+ Nothing rather than a refusal, because a read is what a caller does to find out whether there is
27
+ anything there, and a set before start holds nothing in exactly the way an empty one does.
28
+ ]]
29
+ local function viewing (self: any): Sync.Sync?
30
+ local live = self.live
31
+ local session = live.session
32
+ if session == nil then return nil end
33
+
34
+ return session:set (live.id :: number)
35
+ end
36
+
37
+ -- A walk over nothing, which is what a set that has not opened has to walk.
38
+ local function barren (): (number?, any) return nil end
39
+
40
+ --[[
41
+ A callback, attached whenever the caller liked.
42
+
43
+ Set callbacks attach at any time for the reason listeners do, and the one that most needs it is
44
+ `onAdded`: a client attaching after start would miss the seeding its own join asked for.
45
+ ]]
46
+ local function watches (
47
+ self: any,
48
+ attach: (Sync.Sync, any) -> Types.Connection,
49
+ fn: any
50
+ ): Types.Connection
51
+ local live = self.live
52
+ return Handles.later (
53
+ live,
54
+ function (session) return attach (session:set (live.id :: number), fn) end
55
+ )
56
+ end
57
+
58
+ -- Public --------------------------------------------------------------
59
+
60
+ -- What clears an optional field, since nothing else can.
61
+ Set.NONE = Sync.NONE
62
+
63
+ --[[
64
+ The key is not here. A set is split by a field a caller names afterwards, and until they do the
65
+ whole set goes to everybody, which is the declaration this makes on its own.
66
+ ]]
67
+ function Set.new <T>(of: Types.Codec<T>): Types.Set<T>
68
+ --[[
69
+ Level two is the caller, so a set refused at start is named against the line that declared
70
+ it rather than against the start that did the refusing.
71
+ ]]
72
+ local file, line = debug.info (2, "sl")
73
+ local self = {
74
+ kind = "set" :: "set",
75
+ of = of,
76
+ keyField = nil :: string?,
77
+ at = { file = file, line = line },
78
+ live = Handles.live (),
79
+ }
80
+
81
+ return (setmetatable (self, Set) :: any) :: Types.Set<T>
82
+ end
83
+
84
+ --[[
85
+ The field records are split by, which is what gives a key an audience to be assigned one.
86
+
87
+ Whether the field exists and whether its codec can be a key is settled at start against the
88
+ compiled record rather than here against the declaration, which has no record to look in yet.
89
+ ]]
90
+ function Set.keyBy (self: any, field: string): any
91
+ Define.declaring (self.live, "keyBy")
92
+ self.keyField = field
93
+ return self
94
+ end
95
+
96
+ -- The full record, under an id nothing else in this set holds.
97
+ function Set.add (self: any, id: number, record: any)
98
+ local of = owning (self, "add")
99
+ Sync.add (of, id, record)
100
+ end
101
+
102
+ -- Only the fields named, and only to whoever can already see the record they are on.
103
+ function Set.update (self: any, id: number, fields: any)
104
+ local of = owning (self, "update")
105
+ Sync.update (of, id, fields)
106
+ end
107
+
108
+ function Set.remove (self: any, id: number)
109
+ local of = owning (self, "remove")
110
+ Sync.remove (of, id)
111
+ end
112
+
113
+ --[[
114
+ Everything at once, which goes out as one marker per partition rather than a removal per id.
115
+ A listener is still told once per record, under a cause that says a clear was what did it.
116
+ ]]
117
+ function Set.clear (self: any)
118
+ local of = owning (self, "clear")
119
+ Sync.clear (of)
120
+ end
121
+
122
+ --[[
123
+ Who sees the records under one key, which an unkeyed set has nobody to say it about.
124
+
125
+ Refused rather than ignored: an audience on a set that goes to everybody is a caller who
126
+ believes they have restricted something and has not.
127
+ ]]
128
+ function Set.audience (self: any, key: any, to: Types.Recipient)
129
+ local of = owning (self, "audience")
130
+ if self.keyField == nil then
131
+ Define.refuse (
132
+ "audience is for a set split by keyBy, and this one goes to everybody",
133
+ Define.naming (self.live, "audience")
134
+ )
135
+ end
136
+
137
+ Sync.audience (of, key, to)
138
+ end
139
+
140
+ --[[
141
+ The live record under an id, borrowed rather than copied.
142
+
143
+ Nothing where this machine holds no record under it, which on a client is every id its audiences
144
+ do not reach as well as every id nobody has added.
145
+ ]]
146
+ function Set.get (self: any, id: number): any?
147
+ local of = viewing (self)
148
+ if of == nil then return nil end
149
+
150
+ return Sync.get (of, id)
151
+ end
152
+
153
+ function Set.__len (self: any): number
154
+ local of = viewing (self)
155
+ if of == nil then return 0 end
156
+
157
+ return Sync.size (of)
158
+ end
159
+
160
+ -- Every record this machine holds, in no order a caller may rely on.
161
+ function Set.__iter (self: any): (any, any, any)
162
+ local of = viewing (self)
163
+ if of == nil then return barren, self, nil end
164
+
165
+ return Sync.walk (of), self, nil
166
+ end
167
+
168
+ function Set.onAdded (self: any, fn: (number, any) -> ()): Types.Connection
169
+ return watches (self, Sync.onAdded, fn)
170
+ end
171
+
172
+ function Set.onChanged (self: any, fn: (number, any, any?) -> ()): Types.Connection
173
+ return watches (self, Sync.onChanged, fn)
174
+ end
175
+
176
+ function Set.onRemoved (self: any, fn: (number, Types.Cause) -> ()): Types.Connection
177
+ return watches (self, Sync.onRemoved, fn)
178
+ end
179
+
180
+ -- The schema as text, which is how a record turned away on arrival is described back.
181
+ function Set.describe (self: any): string
182
+ return Serial.frame ({
183
+ kind = "set",
184
+ of = { cell = Base.cell (self.of) },
185
+ keyField = self.keyField,
186
+ })
187
+ end
188
+
189
+ return table.freeze (Set)
@@ -0,0 +1,124 @@
1
+ --!strict
2
+ --!optimize 2
3
+ -- One block of a frame: the bytes a producer laid down, and how many places still hold them.
4
+
5
+ local Bytes = require (script.Parent.Parent.wire.Bytes)
6
+
7
+ local Blocks = {}
8
+
9
+ -- Constants -----------------------------------------------------------
10
+
11
+ --[[
12
+ The five classes a reliable frame's blocks drain in, which is that drain order as the buckets a
13
+ queue has.
14
+ ]]
15
+ const RESPONSE = 1
16
+ const REQUEST = RESPONSE + 1
17
+ const PACKET = REQUEST + 1
18
+ const ESTABLISH = PACKET + 1
19
+ const DELTA = ESTABLISH + 1
20
+
21
+ -- What a block on the lossy lane is classed as, which is nothing.
22
+ const LOOSE = 0
23
+
24
+ -- What a released block points at until its next build, so a pooled record pins no bytes.
25
+ const EMPTY = buffer.create (0)
26
+
27
+ -- Types ---------------------------------------------------------------
28
+
29
+ -- One block, queued and copied and eventually let go.
30
+ export type Block = {
31
+ buf: buffer,
32
+ len: number,
33
+ class: number,
34
+ seq: number,
35
+ refs: { Instance }?,
36
+ rc: number,
37
+ }
38
+
39
+ -- State ---------------------------------------------------------------
40
+
41
+ -- Blocks nobody holds any more, kept for the next build rather than collected.
42
+ local free: { Block } = {}
43
+
44
+ -- Public --------------------------------------------------------------
45
+
46
+ Blocks.RESPONSE = RESPONSE
47
+ Blocks.REQUEST = REQUEST
48
+ Blocks.PACKET = PACKET
49
+ Blocks.ESTABLISH = ESTABLISH
50
+ Blocks.DELTA = DELTA
51
+ Blocks.LOOSE = LOOSE
52
+
53
+ -- How many classes there are, which is the number of buckets a reliable queue keeps.
54
+ Blocks.CLASSES = DELTA
55
+
56
+ -- What a caller runs to fill one block, which is everything this layer refuses to know.
57
+ export type Produce = (b: buffer, pos: number) -> number
58
+
59
+ --[[
60
+ One block over bytes already in scratch, sealed at a length rather than run from a producer,
61
+ because a planner writes a section to decide whether it fits and the frame has moved on by the
62
+ time the block is wanted. What is sealed is exactly the bytes written, so a frame is the sum of
63
+ its blocks, and `refs` is copied because the array it came from is about to be refilled.
64
+ ]]
65
+ function Blocks.sealed (
66
+ into: buffer,
67
+ len: number,
68
+ class: number,
69
+ seq: number,
70
+ refs: { Instance }?
71
+ ): Block
72
+ local held = if refs == nil then nil else table.clone (refs)
73
+ local sealed = Bytes.seal (into, len)
74
+
75
+ local of = table.remove (free)
76
+ if of == nil then
77
+ return { buf = sealed, len = len, class = class, seq = seq, refs = held, rc = 1 }
78
+ end
79
+
80
+ of.buf = sealed
81
+ of.len = len
82
+ of.class = class
83
+ of.seq = seq
84
+ of.refs = held
85
+ of.rc = 1
86
+ return of
87
+ end
88
+
89
+ -- One block, built by running a producer into scratch and sealing what it wrote.
90
+ function Blocks.build (
91
+ into: buffer,
92
+ class: number,
93
+ seq: number,
94
+ refs: { Instance }?,
95
+ produce: Produce
96
+ ): Block
97
+ return Blocks.sealed (into, produce (into, 0), class, seq, refs)
98
+ end
99
+
100
+ -- One more hold, taken by whatever is about to keep a block past the call that handed it over.
101
+ function Blocks.retain (of: Block)
102
+ of.rc += 1
103
+ end
104
+
105
+ -- One hold fewer, and the pool where the last one leaves it.
106
+ function Blocks.release (of: Block)
107
+ of.rc -= 1
108
+ if of.rc > 0 then return end
109
+
110
+ of.buf = EMPTY
111
+ of.len = 0
112
+ of.refs = nil
113
+ table.insert (free, of)
114
+ end
115
+
116
+ -- Every block in a list let go, and the list emptied.
117
+ function Blocks.drain (held: { Block })
118
+ for _, of in held do
119
+ Blocks.release (of)
120
+ end
121
+ table.clear (held)
122
+ end
123
+
124
+ return table.freeze (Blocks)
@@ -0,0 +1,118 @@
1
+ --!strict
2
+ --!optimize 2
3
+ -- A membership set a caller mutates and an audience holds, and the sweep that thins one.
4
+
5
+ local Log = require (script.Parent.Parent.kernel.Log)
6
+ local Types = require (script.Parent.Parent.Types)
7
+
8
+ local Group = {}
9
+
10
+ -- Types ---------------------------------------------------------------
11
+
12
+ --[[
13
+ A set kept as both an array and the place each member sits in it. The array is what a walk and a
14
+ count read, and the index is what a membership test and a removal read, and neither is derivable
15
+ from the other in constant time.
16
+ ]]
17
+ type Held = {
18
+ members: { Player },
19
+ at: { [Player]: number },
20
+ }
21
+
22
+ -- Private -------------------------------------------------------------
23
+
24
+ local Members = {}
25
+ Members.__index = Members
26
+
27
+ --[[
28
+ What a group becomes when it is released. Every read and every write goes through a metamethod
29
+ that raises, so a caller holding a stale reference is told rather than answered: a destroyed
30
+ group that reported an empty membership would let them fire at nobody forever and never learn
31
+ why.
32
+ ]]
33
+ local Dead = {}
34
+
35
+ local function gone (): any return Log.refuse ("this group was destroyed") end
36
+
37
+ Dead.__index = gone
38
+ Dead.__len = gone
39
+ Dead.__iter = gone
40
+
41
+ -- State ---------------------------------------------------------------
42
+
43
+ -- Every group that has not been destroyed, which is where a leave sweep finds them.
44
+ local live: { [Held]: true } = {}
45
+
46
+ -- Public --------------------------------------------------------------
47
+
48
+ --[[
49
+ Adding a member twice leaves one and removing an absent one is nothing. A caller adds on every
50
+ join and removes on every leave from code that has no idea what the group held, so counting
51
+ duplicates or refusing an absent removal would make each of those sites read it first.
52
+ ]]
53
+ function Members.add (self: Held, player: Player)
54
+ if self.at[player] ~= nil then return end
55
+
56
+ table.insert (self.members, player)
57
+ self.at[player] = #self.members
58
+ end
59
+
60
+ -- The last member fills the gap, so a removal costs nothing and the order is not one to rely on.
61
+ function Members.remove (self: Held, player: Player)
62
+ local index = self.at[player]
63
+ if index == nil then return end
64
+
65
+ local last = #self.members
66
+ local moved = self.members[last]
67
+
68
+ self.members[index] = moved
69
+ self.at[moved] = index
70
+
71
+ self.members[last] = nil
72
+ self.at[player] = nil
73
+ end
74
+
75
+ function Members.has (self: Held, player: Player): boolean return self.at[player] ~= nil end
76
+
77
+ --[[
78
+ Emptied and let go, in that order. The emptying is what releases the members: a group is the
79
+ only thing holding some of them by the time a caller is done with it, and waiting for the
80
+ collector to notice is exactly the timing this library never rides.
81
+ ]]
82
+ function Members.destroy (self: Held)
83
+ live[self] = nil
84
+
85
+ table.clear (self.members)
86
+ table.clear (self.at)
87
+ setmetatable (self :: any, Dead)
88
+ end
89
+
90
+ function Members.__len (self: Held): number return #self.members end
91
+
92
+ function Members.__iter (self: Held) return ipairs (self.members) end
93
+
94
+ function Group.new (): Types.Group
95
+ local self: Held = setmetatable ({ members = {}, at = {} }, Members) :: any
96
+ live[self] = true
97
+
98
+ return (self :: any) :: Types.Group
99
+ end
100
+
101
+ --[[
102
+ Whether a value is a group, which is what a recipient asks before it walks one. A destroyed one
103
+ never reaches here: every recipient form is told apart by reading a field off it first, and
104
+ reading any field off a destroyed group is already the raise that says so.
105
+ ]]
106
+ function Group.is (value: unknown): boolean return getmetatable (value :: any) == Members end
107
+
108
+ --[[
109
+ Every group a departed player was in, thinned. Called once per leave from wherever the removal
110
+ is heard, which is the same sweep that releases every other structure keyed by them.
111
+ ]]
112
+ function Group.dropped (player: Player)
113
+ for of in live do
114
+ (of :: any):remove (player)
115
+ end
116
+ end
117
+
118
+ return table.freeze (Group)