@trioforge/crucible 0.1.0 → 0.3.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 (145) hide show
  1. package/maths/spline/package.json +4 -0
  2. package/out/client/index.d.ts +7 -0
  3. package/out/client/init.luau +36 -1
  4. package/out/configs.d.ts +37 -0
  5. package/out/configs.luau +46 -0
  6. package/out/data.d.ts +224 -0
  7. package/out/data.luau +97 -0
  8. package/out/emit/configs.d.ts +4 -0
  9. package/out/emit/configs.luau +22 -0
  10. package/out/emit/data.d.ts +6 -0
  11. package/out/emit/data.luau +19 -0
  12. package/out/emit/index.d.ts +5 -1
  13. package/out/emit/init.luau +23 -7
  14. package/out/emit/network.d.ts +6 -0
  15. package/out/emit/network.luau +36 -0
  16. package/out/emit/replicated.d.ts +4 -0
  17. package/out/emit/replicated.luau +13 -0
  18. package/out/index.d.ts +8 -0
  19. package/out/init.luau +23 -0
  20. package/out/internal/binding.luau +5 -0
  21. package/out/internal/boot.luau +112 -8
  22. package/out/internal/configs/limits.d.ts +10 -0
  23. package/out/internal/configs/limits.luau +25 -0
  24. package/out/internal/configs/port.d.ts +29 -0
  25. package/out/internal/configs/port.luau +99 -0
  26. package/out/internal/configs/runtime.d.ts +21 -0
  27. package/out/internal/configs/runtime.luau +981 -0
  28. package/out/internal/data/admission.d.ts +1 -0
  29. package/out/internal/data/admission.luau +673 -0
  30. package/out/internal/data/declarations.d.ts +1 -0
  31. package/out/internal/data/declarations.luau +384 -0
  32. package/out/internal/data/envelope.d.ts +1 -0
  33. package/out/internal/data/envelope.luau +318 -0
  34. package/out/internal/data/handles.d.ts +1 -0
  35. package/out/internal/data/handles.luau +255 -0
  36. package/out/internal/data/inbox-transforms.d.ts +1 -0
  37. package/out/internal/data/inbox-transforms.luau +458 -0
  38. package/out/internal/data/inbox.d.ts +1 -0
  39. package/out/internal/data/inbox.luau +437 -0
  40. package/out/internal/data/lease.d.ts +1 -0
  41. package/out/internal/data/lease.luau +304 -0
  42. package/out/internal/data/limits.d.ts +1 -0
  43. package/out/internal/data/limits.luau +234 -0
  44. package/out/internal/data/messages.d.ts +1 -0
  45. package/out/internal/data/messages.luau +69 -0
  46. package/out/internal/data/operation-id.d.ts +1 -0
  47. package/out/internal/data/operation-id.luau +61 -0
  48. package/out/internal/data/paths.d.ts +2 -0
  49. package/out/internal/data/paths.luau +323 -0
  50. package/out/internal/data/port.d.ts +1 -0
  51. package/out/internal/data/port.luau +290 -0
  52. package/out/internal/data/profiles.d.ts +1 -0
  53. package/out/internal/data/profiles.luau +862 -0
  54. package/out/internal/data/receipts.d.ts +1 -0
  55. package/out/internal/data/receipts.luau +190 -0
  56. package/out/internal/data/record-bindings.d.ts +1 -0
  57. package/out/internal/data/record-bindings.luau +92 -0
  58. package/out/internal/data/record-declarations.d.ts +1 -0
  59. package/out/internal/data/record-declarations.luau +140 -0
  60. package/out/internal/data/records.d.ts +1 -0
  61. package/out/internal/data/records.luau +400 -0
  62. package/out/internal/data/runtime.d.ts +1 -0
  63. package/out/internal/data/runtime.luau +918 -0
  64. package/out/internal/data/scheduler.d.ts +1 -0
  65. package/out/internal/data/scheduler.luau +507 -0
  66. package/out/internal/data/session.d.ts +1 -0
  67. package/out/internal/data/session.luau +234 -0
  68. package/out/internal/data/shutdown.d.ts +1 -0
  69. package/out/internal/data/shutdown.luau +91 -0
  70. package/out/internal/data/waiters.d.ts +1 -0
  71. package/out/internal/data/waiters.luau +65 -0
  72. package/out/internal/lifecycle.luau +45 -0
  73. package/out/internal/network/catalog.d.ts +26 -0
  74. package/out/internal/network/catalog.luau +182 -0
  75. package/out/internal/network/claims.d.ts +1 -0
  76. package/out/internal/network/claims.luau +146 -0
  77. package/out/internal/network/client-session.d.ts +1 -0
  78. package/out/internal/network/client-session.luau +405 -0
  79. package/out/internal/network/codec.d.ts +63 -0
  80. package/out/internal/network/codec.luau +1183 -0
  81. package/out/internal/network/envelope.d.ts +1 -0
  82. package/out/internal/network/envelope.luau +247 -0
  83. package/out/internal/network/limits.d.ts +26 -0
  84. package/out/internal/network/limits.luau +55 -0
  85. package/out/internal/network/port.d.ts +1 -0
  86. package/out/internal/network/port.luau +19 -0
  87. package/out/internal/network/roblox-network-port.d.ts +1 -0
  88. package/out/internal/network/roblox-network-port.luau +105 -0
  89. package/out/internal/network/schema.d.ts +72 -0
  90. package/out/internal/network/schema.luau +386 -0
  91. package/out/internal/network/server-session.d.ts +1 -0
  92. package/out/internal/network/server-session.luau +395 -0
  93. package/out/internal/network/session.d.ts +1 -0
  94. package/out/internal/network/session.luau +266 -0
  95. package/out/internal/reactive/context.d.ts +1 -0
  96. package/out/internal/reactive/context.luau +149 -0
  97. package/out/internal/reactive/flush.d.ts +1 -0
  98. package/out/internal/reactive/flush.luau +355 -0
  99. package/out/internal/reactive/graph.d.ts +1 -0
  100. package/out/internal/reactive/graph.luau +626 -0
  101. package/out/internal/reactive/limits.d.ts +1 -0
  102. package/out/internal/reactive/limits.luau +54 -0
  103. package/out/internal/reactive/ownership.d.ts +1 -0
  104. package/out/internal/reactive/ownership.luau +626 -0
  105. package/out/internal/registry.luau +10 -1
  106. package/out/internal/sync/client-sync.d.ts +1 -0
  107. package/out/internal/sync/client-sync.luau +157 -0
  108. package/out/internal/sync/declarations.d.ts +1 -0
  109. package/out/internal/sync/declarations.luau +276 -0
  110. package/out/internal/sync/frame.d.ts +1 -0
  111. package/out/internal/sync/frame.luau +91 -0
  112. package/out/internal/sync/server-sync.d.ts +1 -0
  113. package/out/internal/sync/server-sync.luau +502 -0
  114. package/out/internal/sync/values.d.ts +1 -0
  115. package/out/internal/sync/values.luau +930 -0
  116. package/out/maths/spline/curve.d.ts +35 -0
  117. package/out/maths/spline/curve.luau +1122 -0
  118. package/out/maths/spline/index.d.ts +2 -0
  119. package/out/maths/spline/init.luau +8 -0
  120. package/out/network.d.ts +66 -0
  121. package/out/network.luau +24 -0
  122. package/out/reactive.d.ts +76 -0
  123. package/out/reactive.luau +136 -0
  124. package/out/replicated.d.ts +38 -0
  125. package/out/replicated.luau +45 -0
  126. package/out/server/data.d.ts +54 -0
  127. package/out/server/data.luau +44 -0
  128. package/out/server/index.d.ts +17 -0
  129. package/out/server/init.luau +58 -4
  130. package/out/server/publish.d.ts +20 -0
  131. package/out/server/publish.luau +24 -0
  132. package/out/testing/index.d.ts +38 -0
  133. package/out/testing/init.luau +189 -0
  134. package/out/util/random/index.d.ts +24 -0
  135. package/out/util/random/init.luau +301 -0
  136. package/out/vide/index.d.ts +7 -0
  137. package/out/vide/init.luau +50 -0
  138. package/package.json +28 -2
  139. package/testing/package.json +4 -0
  140. package/transformer/data.cjs +72 -0
  141. package/transformer/declarations.cjs +153 -0
  142. package/transformer/index.js +87 -23
  143. package/transformer/network.cjs +961 -0
  144. package/util/random/package.json +4 -0
  145. package/vide/package.json +4 -0
@@ -0,0 +1,234 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local bootClock = TS.import(script, script.Parent.Parent, "boot").bootClock
4
+ local _graph = TS.import(script, script.Parent.Parent, "reactive", "graph")
5
+ local createStateNode = _graph.createStateNode
6
+ local update = _graph.update
7
+ local call = TS.import(script, script.Parent.Parent, "runner").call
8
+ local admitWrite = TS.import(script, script.Parent, "admission").admitWrite
9
+ local jsonBytes = TS.import(script, script.Parent, "envelope").jsonBytes
10
+ local PAYLOAD_MAX_BYTES = TS.import(script, script.Parent, "limits").PAYLOAD_MAX_BYTES
11
+ --* @internal `confirmed` before any owned write with the session's token is confirmed (rule 37).
12
+ local NONE = -1
13
+ --[[
14
+ *
15
+ * @internal
16
+ * Loading --acquired, onLoad--> Active --leave / handoff / final phase / authority lost--> Sealed --release--> Ended
17
+ * Loading --failed, left or closed: abandon only--> Ended
18
+
19
+ ]]
20
+ --* @internal
21
+ --* @internal How a release settled: its write committed, or it was fenced or gave up (rule 29).
22
+ local callbackThreads = {}
23
+ --* Data never writes this state: its `update` frame is what makes reactive reject a nested write (rule 13, K7).
24
+ local transaction = createStateNode(0)
25
+ local depth = 0
26
+ --[[
27
+ *
28
+ * @internal Runs a data callback on a no-yield worker whose thread may not write data, inside the `update` of a private
29
+ * reactive state so a state, publish or apply write throws (rule 13); a yield or throw is thrown here.
30
+
31
+ ]]
32
+ local function dataCallback(work, a, b)
33
+ local worker
34
+ depth += 1
35
+ local status, value = call(function(x, y)
36
+ worker = coroutine.running()
37
+ local _worker = worker
38
+ callbackThreads[_worker] = true
39
+ local result
40
+ update(transaction, function(current)
41
+ result = work(x, y)
42
+ return current
43
+ end)
44
+ return result
45
+ end, a, b)
46
+ depth -= 1
47
+ if worker ~= nil then
48
+ local _worker = worker
49
+ callbackThreads[_worker] = nil
50
+ end
51
+ if status ~= 0 then
52
+ error(value, 0)
53
+ end
54
+ return value
55
+ end
56
+ --[[
57
+ *
58
+ * @internal True while any data callback runs. Callbacks never yield, so code running then was started inside one,
59
+ * on its thread or a thread it spawned (rule 123).
60
+
61
+ ]]
62
+ local function insideDataCallback()
63
+ return depth > 0
64
+ end
65
+ --* @internal Rule 13: no data write on a data callback's own thread.
66
+ local function assertOutsideCallback()
67
+ local _arg0 = coroutine.running()
68
+ if callbackThreads[_arg0] ~= nil then
69
+ error("a write inside a data callback; return the data instead", 3)
70
+ end
71
+ end
72
+ --* @internal One player's profile on this server, and the only writer of its data (rules 12 to 16).
73
+ local Session
74
+ do
75
+ Session = setmetatable({}, {
76
+ __tostring = function()
77
+ return "Session"
78
+ end,
79
+ })
80
+ Session.__index = Session
81
+ function Session.new(...)
82
+ local self = setmetatable({}, Session)
83
+ return self:constructor(...) or self
84
+ end
85
+ function Session:constructor(schema, player, userId, token, hooks)
86
+ self.schema = schema
87
+ self.player = player
88
+ self.userId = userId
89
+ self.token = token
90
+ self.hooks = hooks
91
+ self.phase = 0
92
+ self.abandoned = false
93
+ self.epoch = 0
94
+ self.safeUntil = 0
95
+ self.revision = 0
96
+ self.confirmed = NONE
97
+ self.op = 0
98
+ self.busy = false
99
+ self.receipts = {}
100
+ self.durable = {}
101
+ self.effects = {}
102
+ self.acks = {}
103
+ self.marks = {}
104
+ self.inbox = {}
105
+ self.retryAt = {}
106
+ self.carrying = NONE
107
+ self.lost = NONE
108
+ self.releaseWanted = false
109
+ self.releaseAmbiguous = false
110
+ self.renewAt = 0
111
+ end
112
+ function Session:writable()
113
+ return self.phase == 1 and bootClock() < self.safeUntil
114
+ end
115
+ function Session:snapshot()
116
+ local applied = {}
117
+ for id, revision in self.acks do
118
+ local _arg0 = { id, revision }
119
+ table.insert(applied, _arg0)
120
+ end
121
+ table.sort(applied, function(a, b)
122
+ return a[2] < b[2]
123
+ end)
124
+ -- ▼ ReadonlyArray.map ▼
125
+ local _newValue = table.create(#applied)
126
+ local _callback = function(_param)
127
+ local id = _param[1]
128
+ return id
129
+ end
130
+ for _k, _v in applied do
131
+ _newValue[_k] = _callback(_v, _k - 1, applied)
132
+ end
133
+ -- ▲ ReadonlyArray.map ▲
134
+ local acks = _newValue
135
+ local marks = {}
136
+ for _, mark in self.marks do
137
+ table.insert(marks, mark)
138
+ end
139
+ local _object = {
140
+ revision = self.revision,
141
+ schemaVersion = self.schema.version,
142
+ payload = self.value,
143
+ }
144
+ local _left = "receipts"
145
+ local _array = {}
146
+ local _length = #_array
147
+ local _array_1 = self.receipts
148
+ table.move(_array_1, 1, #_array_1, _length + 1, _array)
149
+ _object[_left] = _array
150
+ _object.acks = acks
151
+ _object.marks = marks
152
+ return _object
153
+ end
154
+ function Session:uncertain()
155
+ local ticket = self.writing
156
+ local _result = ticket
157
+ if _result ~= nil then
158
+ _result = _result.dispatched
159
+ end
160
+ local _condition = _result == true
161
+ if _condition then
162
+ _condition = (ticket.inFlight or ticket.ambiguous)
163
+ end
164
+ local pending = if _condition then self.carrying else NONE
165
+ return math.max(pending, self.lost)
166
+ end
167
+ function Session:assertIdle()
168
+ if self.busy then
169
+ error(`{self.schema.name}: {self.userId}'s profile is inside a data callback; return the data instead`, 3)
170
+ end
171
+ end
172
+ function Session:transact(work)
173
+ self:assertIdle()
174
+ self.busy = true
175
+ local ok, result = pcall(work)
176
+ self.busy = false
177
+ self.hooks.changed()
178
+ if not ok then
179
+ error(result, 0)
180
+ end
181
+ return result
182
+ end
183
+ function Session:callback(work, a, b)
184
+ return dataCallback(work, a, b)
185
+ end
186
+ function Session:prepare(value)
187
+ local admitted = admitWrite(self.schema, value)
188
+ local bytes = jsonBytes(admitted)
189
+ if bytes > PAYLOAD_MAX_BYTES then
190
+ error(`{self.schema.name}: {self.userId}'s profile is {bytes} bytes, over {PAYLOAD_MAX_BYTES}`, 0)
191
+ end
192
+ local _result = self.schema.check
193
+ if _result ~= nil then
194
+ _result(admitted, self.value)
195
+ end
196
+ return admitted
197
+ end
198
+ function Session:commit(value, stage)
199
+ self.value = value
200
+ local _result = stage
201
+ if _result ~= nil then
202
+ _result()
203
+ end
204
+ self.revision += 1
205
+ end
206
+ function Session:write(change, stage)
207
+ return self:transact(function()
208
+ local current = self.value
209
+ local result = self:callback(change, current, nil)
210
+ if rawequal(result, current) and stage == nil then
211
+ return false
212
+ end
213
+ self:commit(self:prepare(result), stage)
214
+ self.hooks.publish(self.value)
215
+ return true
216
+ end)
217
+ end
218
+ function Session:stage(id)
219
+ local _receipts = self.receipts
220
+ local _id = id
221
+ if not (table.find(_receipts, _id) ~= nil) then
222
+ local _receipts_1 = self.receipts
223
+ local _id_1 = id
224
+ table.insert(_receipts_1, _id_1)
225
+ end
226
+ end
227
+ end
228
+ return {
229
+ dataCallback = dataCallback,
230
+ insideDataCallback = insideDataCallback,
231
+ assertOutsideCallback = assertOutsideCallback,
232
+ NONE = NONE,
233
+ Session = Session,
234
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,91 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local _boot = TS.import(script, script.Parent.Parent, "boot")
4
+ local bootClock = _boot.bootClock
5
+ local shutdownProgress = _boot.shutdownProgress
6
+ local _limits = TS.import(script, script.Parent, "limits")
7
+ local DISPATCH_CUTOFF_SECONDS = _limits.DISPATCH_CUTOFF_SECONDS
8
+ local FINAL_PHASE_SECONDS = _limits.FINAL_PHASE_SECONDS
9
+ local SETTLE_MARGIN_SECONDS = _limits.SETTLE_MARGIN_SECONDS
10
+ local SHUTDOWN_FALLBACK_SECONDS = _limits.SHUTDOWN_FALLBACK_SECONDS
11
+ local function pending(engine)
12
+ for _, runtime in engine.profiles do
13
+ for _1, session in runtime.sessions do
14
+ if session.phase ~= 3 then
15
+ return true
16
+ end
17
+ end
18
+ end
19
+ return false
20
+ end
21
+ --[[
22
+ *
23
+ * @internal DataRuntime's onShutdown (rules 50 to 56), against core's deadline through E-D1. `subsystem` is the hook's
24
+ * own Subsystem, excluded from `pending`; with no visible sweep the window is SHUTDOWN_FALLBACK_SECONDS.
25
+
26
+ ]]
27
+ local function shutdownData(engine, subsystem)
28
+ local port = engine.port
29
+ local progress = if subsystem == nil then nil else shutdownProgress(subsystem)
30
+ local _result = progress
31
+ if _result ~= nil then
32
+ _result = _result.deadline
33
+ end
34
+ local _condition = _result
35
+ if _condition == nil then
36
+ _condition = bootClock() + SHUTDOWN_FALLBACK_SECONDS
37
+ end
38
+ local deadline = _condition
39
+ local finalAt = deadline - FINAL_PHASE_SECONDS
40
+ local cutoff = deadline - DISPATCH_CUTOFF_SECONDS
41
+ local settleAt = deadline - SETTLE_MARGIN_SECONDS
42
+ engine.scheduler:setDispatchCutoff(cutoff)
43
+ engine.closing = true
44
+ engine.receipts:close()
45
+ for _, runtime in engine.profiles do
46
+ for _1, session in runtime.sessions do
47
+ if session.phase == 0 then
48
+ session.abandoned = true
49
+ elseif session.phase == 1 and session.revision > session.confirmed then
50
+ runtime:flush(session)
51
+ end
52
+ end
53
+ end
54
+ engine:wake()
55
+ local others = function()
56
+ local _result_1
57
+ if subsystem == nil then
58
+ _result_1 = 0
59
+ else
60
+ local _result_2 = shutdownProgress(subsystem)
61
+ if _result_2 ~= nil then
62
+ _result_2 = _result_2.pending
63
+ end
64
+ local _condition_1 = _result_2
65
+ if _condition_1 == nil then
66
+ _condition_1 = 0
67
+ end
68
+ _result_1 = _condition_1
69
+ end
70
+ return _result_1
71
+ end
72
+ while bootClock() < finalAt and others() > 0 do
73
+ port.wait(0)
74
+ end
75
+ engine.finalPhase = true
76
+ for _, runtime in engine.profiles do
77
+ for _1, session in runtime.sessions do
78
+ if session.phase == 1 then
79
+ runtime:release(session, "shutdown")
80
+ end
81
+ end
82
+ end
83
+ while bootClock() < settleAt and pending(engine) do
84
+ port.wait(0)
85
+ end
86
+ engine.settled = true
87
+ engine:wake()
88
+ end
89
+ return {
90
+ shutdownData = shutdownData,
91
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,65 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local bootClock = TS.import(script, script.Parent.Parent, "boot").bootClock
4
+ --[[
5
+ *
6
+ * @internal Threads parked until a wake or their deadline. Callers re-check their condition in a loop, so a spurious
7
+ * wake costs one check. A park id guards each resume, so a stale deadline timer never resumes a later park.
8
+
9
+ ]]
10
+ local Waiters
11
+ do
12
+ Waiters = setmetatable({}, {
13
+ __tostring = function()
14
+ return "Waiters"
15
+ end,
16
+ })
17
+ Waiters.__index = Waiters
18
+ function Waiters.new(...)
19
+ local self = setmetatable({}, Waiters)
20
+ return self:constructor(...) or self
21
+ end
22
+ function Waiters:constructor(port)
23
+ self.port = port
24
+ self.parked = {}
25
+ self.sequence = 0
26
+ end
27
+ function Waiters:park(deadline)
28
+ local thread = coroutine.running()
29
+ self.sequence += 1
30
+ local id = self.sequence
31
+ self.parked[thread] = id
32
+ local fired = false
33
+ local timer = if deadline == nil then nil else self.port.delay(math.max(deadline - bootClock(), 0), function()
34
+ fired = true
35
+ if self.parked[thread] ~= id then
36
+ return nil
37
+ end
38
+ self.parked[thread] = nil
39
+ self.port.resume(thread)
40
+ end)
41
+ coroutine.yield()
42
+ self.parked[thread] = nil
43
+ if timer ~= nil and not fired then
44
+ self.port.cancel(timer)
45
+ end
46
+ end
47
+ function Waiters:wake()
48
+ local threads = {}
49
+ for thread, id in self.parked do
50
+ local _arg0 = { thread, id }
51
+ table.insert(threads, _arg0)
52
+ end
53
+ table.clear(self.parked)
54
+ table.sort(threads, function(a, b)
55
+ return a[2] < b[2]
56
+ end)
57
+ for _, _binding in threads do
58
+ local thread = _binding[1]
59
+ self.port.resume(thread)
60
+ end
61
+ end
62
+ end
63
+ return {
64
+ Waiters = Waiters,
65
+ }
@@ -73,6 +73,11 @@ local function stateOf(object)
73
73
  local _object = object
74
74
  return states[_object]
75
75
  end
76
+ local enablingNow
77
+ --* @internal The generation whose Enable step is running. What it creates belongs to its enable interval.
78
+ local function enabling()
79
+ return enablingNow
80
+ end
76
81
  --* @internal Awake, not faulted, and not asked to go. `get` and queries see only live objects.
77
82
  local function isLive(s)
78
83
  return s.fault == nil and s.want ~= 2 and (s.phase == 2 or s.phase == 3 or s.phase == 4)
@@ -168,6 +173,10 @@ local function release(s)
168
173
  local _object = s.object
169
174
  states[_object] = nil
170
175
  end
176
+ local disabled = effects().disabled
177
+ if disabled ~= nil then
178
+ pcall(disabled, s)
179
+ end
171
180
  local subscriptions = s.subscriptions
172
181
  s.subscriptions = nil
173
182
  if subscriptions == nil then
@@ -247,7 +256,10 @@ local function perform(s, step)
247
256
  end
248
257
  if step == 2 then
249
258
  s.phase = 3
259
+ local previous = enablingNow
260
+ enablingNow = s
250
261
  run(s, 2)
262
+ enablingNow = previous
251
263
  return nil
252
264
  end
253
265
  if step == 3 then
@@ -289,6 +301,10 @@ local function perform(s, step)
289
301
  if step == 8 then
290
302
  s.phase = 4
291
303
  run(s, 4)
304
+ local _result = fx.disabled
305
+ if _result ~= nil then
306
+ _result(s)
307
+ end
292
308
  return nil
293
309
  end
294
310
  if step == 9 then
@@ -386,6 +402,13 @@ do
386
402
  function Reconciler:isDraining()
387
403
  return self.depth > 0
388
404
  end
405
+ function Reconciler:open()
406
+ self.depth += 1
407
+ end
408
+ function Reconciler:leave()
409
+ self.depth -= 1
410
+ return self.depth == 0 and (#self.stack > 0 or #self.fresh > 0 or self.pairsQueued)
411
+ end
389
412
  function Reconciler:enter(callback, a, b, owner)
390
413
  self.depth += 1
391
414
  local result, value = call(callback, a, b, owner)
@@ -760,6 +783,20 @@ local reconciler = Reconciler.new()
760
783
  local function isDraining()
761
784
  return reconciler:isDraining()
762
785
  end
786
+ --[[
787
+ *
788
+ * @internal
789
+ * Opens a scope like `within` for a caller that runs its own worker: requests made until `closeScope` wait for it.
790
+ * Pair the two, with nothing between them that can throw.
791
+
792
+ ]]
793
+ local function openScope()
794
+ reconciler:open()
795
+ end
796
+ --* @internal Closes the scope `openScope` opened. True when it was the outermost and requests wait: call `drain`.
797
+ local function closeScope()
798
+ return reconciler:leave()
799
+ end
763
800
  --[[
764
801
  *
765
802
  * @internal
@@ -841,15 +878,22 @@ end
841
878
  local function abandon()
842
879
  reconciler:abandon()
843
880
  end
881
+ --* @internal Forwards to the installed effects' `defer`, so networking defers ClientReady cleanups as lifecycle does.
882
+ local function defer(callback)
883
+ effects().defer(callback)
884
+ end
844
885
  return {
845
886
  nextStep = nextStep,
846
887
  useEffects = useEffects,
847
888
  stateOf = stateOf,
889
+ enabling = enabling,
848
890
  isLive = isLive,
849
891
  hasEnded = hasEnded,
850
892
  track = track,
851
893
  current = current,
852
894
  isDraining = isDraining,
895
+ openScope = openScope,
896
+ closeScope = closeScope,
853
897
  drain = drain,
854
898
  notePairs = notePairs,
855
899
  within = within,
@@ -858,6 +902,7 @@ return {
858
902
  finishStart = finishStart,
859
903
  admit = admit,
860
904
  abandon = abandon,
905
+ defer = defer,
861
906
  MAX_TRANSITIONS = MAX_TRANSITIONS,
862
907
  MAX_AWAKE_WAVES = MAX_AWAKE_WAVES,
863
908
  MAX_BATCHES = MAX_BATCHES,
@@ -0,0 +1,26 @@
1
+ import { type Codec } from "./codec";
2
+ import type { Node } from "./schema";
3
+ export type MessageKind = "to-server" | "to-client" | "ask-server";
4
+ export interface MessageSpec {
5
+ readonly kind: MessageKind;
6
+ readonly name: string;
7
+ readonly input: Node;
8
+ readonly output?: Node;
9
+ readonly timeout?: number;
10
+ }
11
+ export interface Definition {
12
+ readonly kind: MessageKind;
13
+ readonly name: string;
14
+ readonly input: Codec;
15
+ readonly output?: Codec;
16
+ readonly timeout: number;
17
+ readonly index?: number;
18
+ }
19
+ export declare function define(spec: MessageSpec): Definition;
20
+ export declare function seal(): string;
21
+ export declare function adopt(catalog: string): void;
22
+ export declare function definitionAt(index: number): Definition | undefined;
23
+ export declare function definitionOf(token: unknown): Definition | undefined;
24
+ export declare function discardIndices(): void;
25
+ export declare function unpublished(): readonly Definition[];
26
+ export declare function catalogSize(): number;
@@ -0,0 +1,182 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local compile = TS.import(script, script.Parent, "codec").compile
4
+ local L = TS.import(script, script.Parent, "limits")
5
+ local definitions = {}
6
+ local tokens = {}
7
+ --* The framework's own messages: the only names under the reserved `Crucible.` segment (NET 3, rule 55).
8
+ local FRAMEWORK_MESSAGES = {
9
+ ["Crucible.State.Fault"] = true,
10
+ ["Crucible.State.Frame"] = true,
11
+ }
12
+ local names = {}
13
+ local indices = {}
14
+ local sealed = false
15
+ local function validName(name)
16
+ local _name = name
17
+ local _condition = type(_name) == "string"
18
+ if _condition then
19
+ _condition = #name > 0 and #name <= L.MESSAGE_NAME_MAX_BYTES
20
+ end
21
+ assert(_condition, "invalid message name")
22
+ local segments = string.split(name, ".")
23
+ local _condition_1 = segments[1] ~= "Crucible"
24
+ if not _condition_1 then
25
+ local _name_1 = name
26
+ _condition_1 = FRAMEWORK_MESSAGES[_name_1] ~= nil
27
+ end
28
+ assert(_condition_1, "reserved message name")
29
+ for _, segment in segments do
30
+ local _arg0 = (string.match(segment, "^[A-Za-z][A-Za-z0-9_]*$")) ~= nil
31
+ assert(_arg0, "invalid message name")
32
+ end
33
+ end
34
+ local register
35
+ local function define(spec)
36
+ validName(spec.name)
37
+ local _name = spec.name
38
+ local _arg0 = not (FRAMEWORK_MESSAGES[_name] ~= nil)
39
+ assert(_arg0, "reserved message name")
40
+ return register(spec)
41
+ end
42
+ --* @internal The trusted path: a framework message under the reserved `Crucible.` segment that game names refuse.
43
+ local function defineFramework(spec)
44
+ local _name = spec.name
45
+ local _arg0 = FRAMEWORK_MESSAGES[_name] ~= nil
46
+ assert(_arg0, "not a framework message")
47
+ return register(spec)
48
+ end
49
+ function register(spec)
50
+ local _arg0 = not sealed
51
+ local _arg1 = `{spec.name} was declared after the network catalog sealed; declare messages in modules that boot loads.`
52
+ assert(_arg0, _arg1)
53
+ local _name = spec.name
54
+ local _arg0_1 = not (definitions[_name] ~= nil)
55
+ local _arg1_1 = `duplicate message {spec.name}`
56
+ assert(_arg0_1, _arg1_1)
57
+ local _arg0_2 = spec.kind == "to-server" or spec.kind == "to-client" or spec.kind == "ask-server"
58
+ assert(_arg0_2, "invalid message kind")
59
+ local _arg0_3 = if spec.kind == "ask-server" then spec.output ~= nil else spec.output == nil and spec.timeout == nil
60
+ assert(_arg0_3, "invalid message options")
61
+ local _condition = spec.timeout
62
+ if _condition == nil then
63
+ _condition = L.DEFAULT_ASK_TIMEOUT_SECONDS
64
+ end
65
+ local timeout = _condition
66
+ local _arg0_4 = timeout > 0 and timeout <= L.MAX_ASK_TIMEOUT_SECONDS
67
+ assert(_arg0_4, "invalid ask timeout")
68
+ local tally = {
69
+ nodes = 0,
70
+ }
71
+ local definition = table.freeze(setmetatable({
72
+ kind = spec.kind,
73
+ name = spec.name,
74
+ input = compile(spec.input, tally),
75
+ output = if spec.output == nil then nil else compile(spec.output, tally),
76
+ timeout = timeout,
77
+ }, {
78
+ __index = function(_, key)
79
+ local _result
80
+ if key == "index" then
81
+ local _indices = indices
82
+ local _name_1 = spec.name
83
+ _result = _indices[_name_1]
84
+ else
85
+ _result = nil
86
+ end
87
+ return _result
88
+ end,
89
+ }))
90
+ local _name_1 = spec.name
91
+ definitions[_name_1] = definition
92
+ tokens[definition] = true
93
+ return definition
94
+ end
95
+ local function setNames(ordered)
96
+ names = ordered
97
+ indices = {}
98
+ for i = 0, #names - 1 do
99
+ local _indices = indices
100
+ local _arg0 = names[i + 1]
101
+ local _i = i
102
+ _indices[_arg0] = _i
103
+ end
104
+ end
105
+ local function seal()
106
+ local _arg0 = not sealed
107
+ assert(_arg0, "catalog already sealed")
108
+ -- ▼ ReadonlyMap.size ▼
109
+ local _size = 0
110
+ for _ in definitions do
111
+ _size += 1
112
+ end
113
+ -- ▲ ReadonlyMap.size ▲
114
+ local _arg0_1 = _size <= L.MAX_MESSAGES
115
+ assert(_arg0_1, "too many messages")
116
+ local ordered = {}
117
+ for name in definitions do
118
+ table.insert(ordered, name)
119
+ end
120
+ table.sort(ordered)
121
+ setNames(ordered)
122
+ sealed = true
123
+ return table.concat(names, "\n")
124
+ end
125
+ local function adopt(catalog)
126
+ local _condition = not sealed
127
+ if _condition then
128
+ local _catalog = catalog
129
+ _condition = type(_catalog) == "string"
130
+ if _condition then
131
+ _condition = #catalog < L.MAX_MESSAGES * (L.MESSAGE_NAME_MAX_BYTES + 1)
132
+ end
133
+ end
134
+ assert(_condition, "invalid catalog")
135
+ local ordered = if catalog == "" then {} else string.split(catalog, "\n")
136
+ local _arg0 = #ordered <= L.MAX_MESSAGES
137
+ assert(_arg0, "too many messages")
138
+ for i = 0, #ordered - 1 do
139
+ validName(ordered[i + 1])
140
+ local _arg0_1 = i == 0 or ordered[i] < ordered[i + 1]
141
+ assert(_arg0_1, "catalog not strictly ascending")
142
+ end
143
+ setNames(ordered)
144
+ end
145
+ local function definitionAt(index)
146
+ if index < 0 or index % 1 ~= 0 or index >= #names then
147
+ return nil
148
+ end
149
+ local _arg0 = names[index + 1]
150
+ return definitions[_arg0]
151
+ end
152
+ local function definitionOf(token)
153
+ local _token = token
154
+ return if tokens[_token] ~= nil then token else nil
155
+ end
156
+ local function discardIndices()
157
+ sealed = false
158
+ setNames({})
159
+ end
160
+ local function unpublished()
161
+ local result = {}
162
+ for _, definition in definitions do
163
+ if definition.index == nil then
164
+ table.insert(result, definition)
165
+ end
166
+ end
167
+ return result
168
+ end
169
+ local function catalogSize()
170
+ return #names
171
+ end
172
+ return {
173
+ define = define,
174
+ defineFramework = defineFramework,
175
+ seal = seal,
176
+ adopt = adopt,
177
+ definitionAt = definitionAt,
178
+ definitionOf = definitionOf,
179
+ discardIndices = discardIndices,
180
+ unpublished = unpublished,
181
+ catalogSize = catalogSize,
182
+ }