@trioforge/crucible 0.0.0-stage → 0.2.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 (183) hide show
  1. package/LICENSE +21 -0
  2. package/client/package.json +4 -0
  3. package/emit/package.json +4 -0
  4. package/out/all.d.ts +7 -0
  5. package/out/all.luau +2 -0
  6. package/out/attribute.d.ts +7 -0
  7. package/out/attribute.luau +7 -0
  8. package/out/client/index.d.ts +10 -0
  9. package/out/client/init.luau +55 -0
  10. package/out/configs.d.ts +37 -0
  11. package/out/configs.luau +46 -0
  12. package/out/crucible-behaviour.d.ts +16 -0
  13. package/out/crucible-behaviour.luau +78 -0
  14. package/out/crucible-object.d.ts +10 -0
  15. package/out/crucible-object.luau +69 -0
  16. package/out/data.d.ts +224 -0
  17. package/out/data.luau +97 -0
  18. package/out/emit/configs.d.ts +4 -0
  19. package/out/emit/configs.luau +22 -0
  20. package/out/emit/data.d.ts +6 -0
  21. package/out/emit/data.luau +19 -0
  22. package/out/emit/index.d.ts +45 -0
  23. package/out/emit/init.luau +101 -0
  24. package/out/emit/network.d.ts +6 -0
  25. package/out/emit/network.luau +36 -0
  26. package/out/emit/replicated.d.ts +4 -0
  27. package/out/emit/replicated.luau +13 -0
  28. package/out/index.d.ts +20 -0
  29. package/out/init.luau +34 -0
  30. package/out/inject.d.ts +1 -0
  31. package/out/inject.luau +7 -0
  32. package/out/internal/binding.d.ts +1 -0
  33. package/out/internal/binding.luau +615 -0
  34. package/out/internal/boot.d.ts +1 -0
  35. package/out/internal/boot.luau +562 -0
  36. package/out/internal/configs/limits.d.ts +10 -0
  37. package/out/internal/configs/limits.luau +25 -0
  38. package/out/internal/configs/port.d.ts +29 -0
  39. package/out/internal/configs/port.luau +99 -0
  40. package/out/internal/configs/runtime.d.ts +21 -0
  41. package/out/internal/configs/runtime.luau +981 -0
  42. package/out/internal/construction-claim.d.ts +1 -0
  43. package/out/internal/construction-claim.luau +26 -0
  44. package/out/internal/data/admission.d.ts +1 -0
  45. package/out/internal/data/admission.luau +673 -0
  46. package/out/internal/data/declarations.d.ts +1 -0
  47. package/out/internal/data/declarations.luau +384 -0
  48. package/out/internal/data/envelope.d.ts +1 -0
  49. package/out/internal/data/envelope.luau +318 -0
  50. package/out/internal/data/handles.d.ts +1 -0
  51. package/out/internal/data/handles.luau +255 -0
  52. package/out/internal/data/inbox-transforms.d.ts +1 -0
  53. package/out/internal/data/inbox-transforms.luau +458 -0
  54. package/out/internal/data/inbox.d.ts +1 -0
  55. package/out/internal/data/inbox.luau +437 -0
  56. package/out/internal/data/lease.d.ts +1 -0
  57. package/out/internal/data/lease.luau +304 -0
  58. package/out/internal/data/limits.d.ts +1 -0
  59. package/out/internal/data/limits.luau +234 -0
  60. package/out/internal/data/messages.d.ts +1 -0
  61. package/out/internal/data/messages.luau +69 -0
  62. package/out/internal/data/operation-id.d.ts +1 -0
  63. package/out/internal/data/operation-id.luau +61 -0
  64. package/out/internal/data/paths.d.ts +2 -0
  65. package/out/internal/data/paths.luau +323 -0
  66. package/out/internal/data/port.d.ts +1 -0
  67. package/out/internal/data/port.luau +290 -0
  68. package/out/internal/data/profiles.d.ts +1 -0
  69. package/out/internal/data/profiles.luau +862 -0
  70. package/out/internal/data/receipts.d.ts +1 -0
  71. package/out/internal/data/receipts.luau +190 -0
  72. package/out/internal/data/record-bindings.d.ts +1 -0
  73. package/out/internal/data/record-bindings.luau +92 -0
  74. package/out/internal/data/record-declarations.d.ts +1 -0
  75. package/out/internal/data/record-declarations.luau +140 -0
  76. package/out/internal/data/records.d.ts +1 -0
  77. package/out/internal/data/records.luau +400 -0
  78. package/out/internal/data/runtime.d.ts +1 -0
  79. package/out/internal/data/runtime.luau +918 -0
  80. package/out/internal/data/scheduler.d.ts +1 -0
  81. package/out/internal/data/scheduler.luau +507 -0
  82. package/out/internal/data/session.d.ts +1 -0
  83. package/out/internal/data/session.luau +234 -0
  84. package/out/internal/data/shutdown.d.ts +1 -0
  85. package/out/internal/data/shutdown.luau +91 -0
  86. package/out/internal/data/waiters.d.ts +1 -0
  87. package/out/internal/data/waiters.luau +65 -0
  88. package/out/internal/dense-list.d.ts +1 -0
  89. package/out/internal/dense-list.luau +155 -0
  90. package/out/internal/diagnostics.d.ts +1 -0
  91. package/out/internal/diagnostics.luau +29 -0
  92. package/out/internal/hooks.d.ts +1 -0
  93. package/out/internal/hooks.luau +53 -0
  94. package/out/internal/implementers.d.ts +1 -0
  95. package/out/internal/implementers.luau +119 -0
  96. package/out/internal/lifecycle.d.ts +1 -0
  97. package/out/internal/lifecycle.luau +912 -0
  98. package/out/internal/log-output.d.ts +1 -0
  99. package/out/internal/log-output.luau +106 -0
  100. package/out/internal/network/catalog.d.ts +26 -0
  101. package/out/internal/network/catalog.luau +182 -0
  102. package/out/internal/network/claims.d.ts +1 -0
  103. package/out/internal/network/claims.luau +154 -0
  104. package/out/internal/network/client-session.d.ts +1 -0
  105. package/out/internal/network/client-session.luau +403 -0
  106. package/out/internal/network/codec.d.ts +63 -0
  107. package/out/internal/network/codec.luau +1183 -0
  108. package/out/internal/network/envelope.d.ts +1 -0
  109. package/out/internal/network/envelope.luau +247 -0
  110. package/out/internal/network/limits.d.ts +26 -0
  111. package/out/internal/network/limits.luau +55 -0
  112. package/out/internal/network/port.d.ts +1 -0
  113. package/out/internal/network/port.luau +19 -0
  114. package/out/internal/network/roblox-network-port.d.ts +1 -0
  115. package/out/internal/network/roblox-network-port.luau +105 -0
  116. package/out/internal/network/schema.d.ts +72 -0
  117. package/out/internal/network/schema.luau +386 -0
  118. package/out/internal/network/server-session.d.ts +1 -0
  119. package/out/internal/network/server-session.luau +393 -0
  120. package/out/internal/network/session.d.ts +1 -0
  121. package/out/internal/network/session.luau +266 -0
  122. package/out/internal/port.d.ts +1 -0
  123. package/out/internal/port.luau +12 -0
  124. package/out/internal/reactive/context.d.ts +1 -0
  125. package/out/internal/reactive/context.luau +149 -0
  126. package/out/internal/reactive/flush.d.ts +1 -0
  127. package/out/internal/reactive/flush.luau +355 -0
  128. package/out/internal/reactive/graph.d.ts +1 -0
  129. package/out/internal/reactive/graph.luau +626 -0
  130. package/out/internal/reactive/limits.d.ts +1 -0
  131. package/out/internal/reactive/limits.luau +54 -0
  132. package/out/internal/reactive/ownership.d.ts +1 -0
  133. package/out/internal/reactive/ownership.luau +626 -0
  134. package/out/internal/registry.d.ts +1 -0
  135. package/out/internal/registry.luau +455 -0
  136. package/out/internal/roblox-port.d.ts +1 -0
  137. package/out/internal/roblox-port.luau +117 -0
  138. package/out/internal/runner.d.ts +1 -0
  139. package/out/internal/runner.luau +86 -0
  140. package/out/internal/scheduler.d.ts +1 -0
  141. package/out/internal/scheduler.luau +207 -0
  142. package/out/internal/state.d.ts +1 -0
  143. package/out/internal/state.luau +41 -0
  144. package/out/internal/sync/client-sync.d.ts +1 -0
  145. package/out/internal/sync/client-sync.luau +157 -0
  146. package/out/internal/sync/declarations.d.ts +1 -0
  147. package/out/internal/sync/declarations.luau +276 -0
  148. package/out/internal/sync/frame.d.ts +1 -0
  149. package/out/internal/sync/frame.luau +91 -0
  150. package/out/internal/sync/server-sync.d.ts +1 -0
  151. package/out/internal/sync/server-sync.luau +502 -0
  152. package/out/internal/sync/values.d.ts +1 -0
  153. package/out/internal/sync/values.luau +930 -0
  154. package/out/log-level.d.ts +7 -0
  155. package/out/log-level.luau +21 -0
  156. package/out/logging.d.ts +34 -0
  157. package/out/logging.luau +233 -0
  158. package/out/network.d.ts +66 -0
  159. package/out/network.luau +24 -0
  160. package/out/reactive.d.ts +76 -0
  161. package/out/reactive.luau +136 -0
  162. package/out/replicated.d.ts +38 -0
  163. package/out/replicated.luau +45 -0
  164. package/out/server/data.d.ts +54 -0
  165. package/out/server/data.luau +44 -0
  166. package/out/server/index.d.ts +18 -0
  167. package/out/server/init.luau +62 -0
  168. package/out/server/publish.d.ts +20 -0
  169. package/out/server/publish.luau +24 -0
  170. package/out/subsystem.d.ts +4 -0
  171. package/out/subsystem.luau +30 -0
  172. package/out/vide/index.d.ts +7 -0
  173. package/out/vide/init.luau +50 -0
  174. package/package.json +52 -5
  175. package/server/package.json +4 -0
  176. package/transformer/data.cjs +72 -0
  177. package/transformer/declarations.cjs +153 -0
  178. package/transformer/diagnostics.cjs +19 -0
  179. package/transformer/index.js +741 -0
  180. package/transformer/network.cjs +961 -0
  181. package/transformer/type-id.cjs +80 -0
  182. package/vide/package.json +4 -0
  183. package/README.md +0 -3
@@ -0,0 +1,862 @@
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 runtimeDiagnostic = TS.import(script, script.Parent.Parent, "diagnostics").runtimeDiagnostic
5
+ local _admission = TS.import(script, script.Parent, "admission")
6
+ local loadRecord = _admission.loadRecord
7
+ local sealSchema = _admission.sealSchema
8
+ local _envelope = TS.import(script, script.Parent, "envelope")
9
+ local jsonBytes = _envelope.jsonBytes
10
+ local parseEnvelope = _envelope.parseEnvelope
11
+ local _lease = TS.import(script, script.Parent, "lease")
12
+ local abandonAcquisition = _lease.abandonAcquisition
13
+ local acquire = _lease.acquire
14
+ local ownedWrite = _lease.ownedWrite
15
+ local Inbox = TS.import(script, script.Parent, "inbox").Inbox
16
+ local _limits = TS.import(script, script.Parent, "limits")
17
+ local CALL_DEADLINE_SECONDS = _limits.CALL_DEADLINE_SECONDS
18
+ local HANDOFF_TIMEOUT_SECONDS = _limits.HANDOFF_TIMEOUT_SECONDS
19
+ local LEASE_SECONDS = _limits.LEASE_SECONDS
20
+ local LOAD_BUSY_MESSAGE = _limits.LOAD_BUSY_MESSAGE
21
+ local LOAD_DEADLINE_SECONDS = _limits.LOAD_DEADLINE_SECONDS
22
+ local LOAD_FAILED_MESSAGE = _limits.LOAD_FAILED_MESSAGE
23
+ local LOAD_POLL_JITTER = _limits.LOAD_POLL_JITTER
24
+ local LOAD_POLL_MIN_SECONDS = _limits.LOAD_POLL_MIN_SECONDS
25
+ local LOAD_POLL_SCHEDULE_SECONDS = _limits.LOAD_POLL_SCHEDULE_SECONDS
26
+ local LOCAL_AUTHORITY_SECONDS = _limits.LOCAL_AUTHORITY_SECONDS
27
+ local RENEW_INTERVAL_SECONDS = _limits.RENEW_INTERVAL_SECONDS
28
+ local RENEW_JITTER_SECONDS = _limits.RENEW_JITTER_SECONDS
29
+ local SAVE_TIMEOUT_DEFAULT_SECONDS = _limits.SAVE_TIMEOUT_DEFAULT_SECONDS
30
+ local SAVE_TIMEOUT_MAX_SECONDS = _limits.SAVE_TIMEOUT_MAX_SECONDS
31
+ local SERVER_UPDATING_MESSAGE = _limits.SERVER_UPDATING_MESSAGE
32
+ local SESSION_ENDED_MESSAGE = _limits.SESSION_ENDED_MESSAGE
33
+ local TAKEOVER_GRACE_SECONDS = _limits.TAKEOVER_GRACE_SECONDS
34
+ local ReceiptRouter = TS.import(script, script.Parent, "receipts").ReceiptRouter
35
+ local KeyedScheduler = TS.import(script, script.Parent, "scheduler").KeyedScheduler
36
+ local _session = TS.import(script, script.Parent, "session")
37
+ local assertOutsideCallback = _session.assertOutsideCallback
38
+ local Session = _session.Session
39
+ local Waiters = TS.import(script, script.Parent, "waiters").Waiters
40
+ --* @internal
41
+ --* @internal
42
+ --* @internal
43
+ --* @internal
44
+ --[[
45
+ *
46
+ * @internal The seams later units fill. D4 runs the claims and owns the slot's node; D2 delivers the inbox and receipts
47
+ * from every envelope the session reads. Each defaults to doing nothing.
48
+
49
+ ]]
50
+ local NO_HOOKS = {
51
+ onLoad = function(_, value)
52
+ return value
53
+ end,
54
+ onRelease = function(_, value)
55
+ return value
56
+ end,
57
+ publish = function() end,
58
+ delivered = function() end,
59
+ }
60
+ --* @internal One server's data engine: its port, its scheduler and its shutdown state (rules 32, 50 to 56, 107).
61
+ local ProfileRuntime
62
+ local DataEngine
63
+ do
64
+ DataEngine = setmetatable({}, {
65
+ __tostring = function()
66
+ return "DataEngine"
67
+ end,
68
+ })
69
+ DataEngine.__index = DataEngine
70
+ function DataEngine.new(...)
71
+ local self = setmetatable({}, DataEngine)
72
+ return self:constructor(...) or self
73
+ end
74
+ function DataEngine:constructor(port)
75
+ self.port = port
76
+ self.profiles = {}
77
+ self.closing = false
78
+ self.finalPhase = false
79
+ self.settled = false
80
+ self.diagnosed = {}
81
+ self.scheduler = KeyedScheduler.new(port, function(key, message)
82
+ return self:diagnose(key, message)
83
+ end)
84
+ self.receipts = ReceiptRouter.new(self)
85
+ end
86
+ function DataEngine:diagnose(key, message)
87
+ local _diagnosed = self.diagnosed
88
+ local _key = key
89
+ if _diagnosed[_key] ~= nil then
90
+ return nil
91
+ end
92
+ local _diagnosed_1 = self.diagnosed
93
+ local _key_1 = key
94
+ _diagnosed_1[_key_1] = true
95
+ runtimeDiagnostic("warning", message)
96
+ end
97
+ function DataEngine:profile(schema, hooks)
98
+ if hooks == nil then
99
+ hooks = NO_HOOKS
100
+ end
101
+ local runtime = ProfileRuntime.new(self, sealSchema(schema), hooks)
102
+ local _exp = self.profiles
103
+ table.insert(_exp, runtime)
104
+ return runtime
105
+ end
106
+ function DataEngine:wake()
107
+ for _, runtime in self.profiles do
108
+ runtime.changed:wake()
109
+ end
110
+ end
111
+ function DataEngine:retire()
112
+ self.settled = true
113
+ for _, runtime in self.profiles do
114
+ for _1, session in runtime.sessions do
115
+ if session.phase == 0 then
116
+ session.abandoned = true
117
+ elseif session.phase == 1 then
118
+ runtime:seal(session, "retired")
119
+ runtime:startRelease(session)
120
+ end
121
+ end
122
+ end
123
+ self.receipts:close(true)
124
+ self:wake()
125
+ end
126
+ end
127
+ --* @internal The durability engine behind one profile store (rules 9 to 49).
128
+ do
129
+ ProfileRuntime = setmetatable({}, {
130
+ __tostring = function()
131
+ return "ProfileRuntime"
132
+ end,
133
+ })
134
+ ProfileRuntime.__index = ProfileRuntime
135
+ function ProfileRuntime.new(...)
136
+ local self = setmetatable({}, ProfileRuntime)
137
+ return self:constructor(...) or self
138
+ end
139
+ function ProfileRuntime:constructor(engine, schema, hooks)
140
+ self.engine = engine
141
+ self.schema = schema
142
+ self.hooks = hooks
143
+ self.sessions = {}
144
+ self.sealed = {}
145
+ self.changed = Waiters.new(engine.port)
146
+ self.inbox = Inbox.new(self)
147
+ end
148
+ function ProfileRuntime:key(userId)
149
+ return tostring(userId)
150
+ end
151
+ function ProfileRuntime:present(player)
152
+ return self.engine.port.players.present(player)
153
+ end
154
+ function ProfileRuntime:accept(session)
155
+ if session.phase ~= 1 then
156
+ return false
157
+ end
158
+ if bootClock() < session.safeUntil then
159
+ return true
160
+ end
161
+ self:lose(session, "authority")
162
+ return false
163
+ end
164
+ function ProfileRuntime:writable(player)
165
+ local _sessions = self.sessions
166
+ local _player = player
167
+ local session = _sessions[_player]
168
+ return session ~= nil and self:accept(session)
169
+ end
170
+ function ProfileRuntime:players()
171
+ local result = {}
172
+ for player, session in self.sessions do
173
+ if session:writable() then
174
+ table.insert(result, player)
175
+ end
176
+ end
177
+ return result
178
+ end
179
+ function ProfileRuntime:write(player, change, stage)
180
+ local _sessions = self.sessions
181
+ local _player = player
182
+ local session = _sessions[_player]
183
+ local _result = session
184
+ if _result ~= nil then
185
+ _result:assertIdle()
186
+ end
187
+ assertOutsideCallback()
188
+ if self.engine.finalPhase then
189
+ self.engine:diagnose(`late:{self.schema.name}`, `{self.schema.name} refused a write after the shutdown final phase; a shutdown hook that writes profiles must finish before the sweep's deadline - 10 s`)
190
+ return false
191
+ end
192
+ if session == nil or not self:accept(session) then
193
+ return false
194
+ end
195
+ session:write(change, stage)
196
+ return true
197
+ end
198
+ function ProfileRuntime:newSession(player)
199
+ local session = Session.new(self.schema, player, player.UserId, self.engine.port.guid(), {
200
+ changed = function()
201
+ return self.changed:wake()
202
+ end,
203
+ publish = function(value)
204
+ return self.hooks.publish(player, value)
205
+ end,
206
+ })
207
+ local _sessions = self.sessions
208
+ local _player = player
209
+ _sessions[_player] = session
210
+ self.changed:wake()
211
+ return session
212
+ end
213
+ function ProfileRuntime:supersedes(userId)
214
+ local result = {}
215
+ local _sealed = self.sealed
216
+ local _userId = userId
217
+ local _condition = _sealed[_userId]
218
+ if _condition == nil then
219
+ _condition = {}
220
+ end
221
+ for token, session in _condition do
222
+ if session.released ~= nil then
223
+ local _arg1 = {
224
+ epoch = session.epoch,
225
+ snapshot = session:snapshot(),
226
+ }
227
+ result[token] = _arg1
228
+ end
229
+ end
230
+ return result
231
+ end
232
+ function ProfileRuntime:stopped(session)
233
+ return session.abandoned or self.engine.closing or not self:present(session.player)
234
+ end
235
+ ProfileRuntime["end"] = function(self, session, mayHaveCommitted, message)
236
+ session.phase = 3
237
+ if mayHaveCommitted then
238
+ self.engine.scheduler:submit({
239
+ store = self.schema.name,
240
+ key = self:key(session.userId),
241
+ priority = 1,
242
+ kind = "update",
243
+ dispatchDeadline = bootClock() + CALL_DEADLINE_SECONDS,
244
+ bytes = 512,
245
+ transform = function()
246
+ return function(current)
247
+ return abandonAcquisition(current, session.userId, session.token, function() end)
248
+ end
249
+ end,
250
+ })
251
+ end
252
+ if message ~= nil then
253
+ self.engine.port.players.kick(session.player, message)
254
+ end
255
+ self:drop(session)
256
+ self.changed:wake()
257
+ return false
258
+ end
259
+ function ProfileRuntime:drop(session)
260
+ local _sessions = self.sessions
261
+ local _player = session.player
262
+ local _condition = _sessions[_player] == session
263
+ if _condition then
264
+ _condition = not self:present(session.player)
265
+ end
266
+ if _condition then
267
+ local _sessions_1 = self.sessions
268
+ local _player_1 = session.player
269
+ _sessions_1[_player_1] = nil
270
+ end
271
+ end
272
+ function ProfileRuntime:load(player, reason)
273
+ local _sessions = self.sessions
274
+ local _player = player
275
+ local existing = _sessions[_player]
276
+ if existing ~= nil and (existing.phase == 0 or existing.phase == 1) then
277
+ return self:await(player)
278
+ end
279
+ local port = self.engine.port
280
+ local deadline = bootClock() + LOAD_DEADLINE_SECONDS
281
+ local session = self:newSession(player)
282
+ local userId = session.userId
283
+ local polls = 0
284
+ local held = false
285
+ local mayHaveCommitted = false
286
+ local earliest
287
+ local envelope
288
+ while envelope == nil do
289
+ if self:stopped(session) then
290
+ return self["end"](self, session, mayHaveCommitted)
291
+ end
292
+ if bootClock() >= deadline then
293
+ return self["end"](self, session, mayHaveCommitted, if held then LOAD_BUSY_MESSAGE else LOAD_FAILED_MESSAGE)
294
+ end
295
+ local decision
296
+ local sentAt = 0
297
+ local ticket = self.engine.scheduler:submit({
298
+ store = self.schema.name,
299
+ key = self:key(userId),
300
+ priority = 4,
301
+ kind = "update",
302
+ dispatchDeadline = deadline,
303
+ bytes = 1024,
304
+ transform = function()
305
+ local input = {
306
+ userId = userId,
307
+ token = session.token,
308
+ serverId = port.serverId,
309
+ jobId = port.jobId,
310
+ schemaVersion = self.schema.version,
311
+ supersedes = self:supersedes(userId),
312
+ wall = port.wall,
313
+ }
314
+ return function(current)
315
+ return acquire(current, input, function(d)
316
+ decision = d
317
+ return decision
318
+ end)
319
+ end
320
+ end,
321
+ onDispatch = function(at)
322
+ sentAt = at
323
+ return sentAt
324
+ end,
325
+ onResult = function(outcome)
326
+ if outcome.kind == "error" and not outcome.ambiguous then
327
+ return nil
328
+ end
329
+ if earliest == nil then
330
+ earliest = sentAt
331
+ end
332
+ mayHaveCommitted = true
333
+ end,
334
+ })
335
+ ticket:onSettled(function()
336
+ return self.changed:wake()
337
+ end)
338
+ while ticket.settled == nil and not self:stopped(session) do
339
+ self.changed:park()
340
+ end
341
+ local settled = ticket.settled
342
+ if settled == nil then
343
+ return self["end"](self, session, true)
344
+ end
345
+ if settled.kind == "done" and settled.outcome.kind == "error" then
346
+ return self["end"](self, session, mayHaveCommitted, LOAD_FAILED_MESSAGE)
347
+ end
348
+ if settled.kind ~= "done" then
349
+ held = false
350
+ if settled.kind == "overloaded" then
351
+ local wake = math.min(deadline, bootClock() + LOAD_POLL_MIN_SECONDS)
352
+ while bootClock() < wake and not self:stopped(session) do
353
+ self.changed:park(wake)
354
+ end
355
+ end
356
+ continue
357
+ end
358
+ local answer = decision
359
+ if answer.kind == "taken" or answer.kind == "already" then
360
+ envelope = answer.envelope
361
+ elseif answer.kind == "newer" then
362
+ return self["end"](self, session, false, SERVER_UPDATING_MESSAGE)
363
+ elseif answer.kind == "unreadable" then
364
+ self.engine:diagnose(`unreadable:{self.schema.name}:{userId}`, `{self.schema.name}: {userId}'s record is unreadable: {answer.reason}`)
365
+ return self["end"](self, session, false, LOAD_FAILED_MESSAGE)
366
+ else
367
+ held = true
368
+ mayHaveCommitted = false
369
+ earliest = nil
370
+ local base = LOAD_POLL_SCHEDULE_SECONDS[math.min(polls, #LOAD_POLL_SCHEDULE_SECONDS - 1) + 1]
371
+ polls += 1
372
+ local jittered = base * (1 - LOAD_POLL_JITTER + 2 * LOAD_POLL_JITTER * port.random())
373
+ local room = answer.lease.expiresAt + TAKEOVER_GRACE_SECONDS - port.wall()
374
+ local wake = math.min(deadline, bootClock() + math.max(LOAD_POLL_MIN_SECONDS, math.min(jittered, room)))
375
+ while bootClock() < wake and not self:stopped(session) do
376
+ self.changed:park(wake)
377
+ end
378
+ end
379
+ end
380
+ session.epoch = envelope.epoch
381
+ session.safeUntil = earliest + LOCAL_AUTHORITY_SECONDS
382
+ local superseded = self.sealed[userId]
383
+ if superseded ~= nil then
384
+ for token, sealed in superseded do
385
+ if sealed.released ~= nil then
386
+ superseded[token] = nil
387
+ end
388
+ end
389
+ end
390
+ local loaded = loadRecord(self.schema, envelope.payload, envelope.schemaVersion)
391
+ if loaded.kind ~= "ok" then
392
+ self.engine:diagnose(`load:{self.schema.name}:{if loaded.kind == "newer" then "newer" else loaded.code}`, if loaded.kind == "newer" then `{self.schema.name}: {userId}'s record was written by a newer build (version {loaded.version})` else `{self.schema.name}: {userId}'s record did not load: {loaded.reason}`)
393
+ return self["end"](self, session, true, if loaded.kind == "newer" then SERVER_UPDATING_MESSAGE else LOAD_FAILED_MESSAGE)
394
+ end
395
+ session.value = loaded.value
396
+ session.revision = if loaded.migrated then 1 else 0
397
+ for _, id in envelope.receipts do
398
+ session:stage(id)
399
+ end
400
+ local _set = {}
401
+ for _, _v in envelope.receipts do
402
+ _set[_v] = true
403
+ end
404
+ session.durable = _set
405
+ local ok, claimed = pcall(function()
406
+ local result = self.hooks.onLoad(session, session.value, reason)
407
+ return if rawequal(result, session.value) then result else session:prepare(result)
408
+ end)
409
+ if not ok then
410
+ self.engine:diagnose(`onLoad:{self.schema.name}:{session.token}`, `{self.schema.name}: {userId}'s onLoad failed ({reason})`)
411
+ return self["end"](self, session, true, LOAD_FAILED_MESSAGE)
412
+ end
413
+ if not rawequal(claimed, session.value) then
414
+ session:commit(claimed)
415
+ end
416
+ if self:stopped(session) then
417
+ return self["end"](self, session, true)
418
+ end
419
+ if bootClock() >= session.safeUntil then
420
+ return self["end"](self, session, true, SESSION_ENDED_MESSAGE)
421
+ end
422
+ session.phase = 1
423
+ self.hooks.publish(player, session.value)
424
+ self.hooks.delivered(session, envelope)
425
+ self.inbox:deliver(session, envelope)
426
+ session.renewAt = bootClock() + port.random() * RENEW_INTERVAL_SECONDS
427
+ self:armSeal(session)
428
+ self:armRenewal(session)
429
+ if session.revision > 0 then
430
+ self:requestWrite(session, 3)
431
+ end
432
+ self.changed:wake()
433
+ return true
434
+ end
435
+ function ProfileRuntime:armSeal(session)
436
+ local port = self.engine.port
437
+ local check
438
+ check = function()
439
+ if session.phase ~= 1 then
440
+ return nil
441
+ end
442
+ local now = bootClock()
443
+ if now < session.safeUntil then
444
+ port.delay(session.safeUntil - now, check)
445
+ return nil
446
+ end
447
+ while session.busy do
448
+ self.changed:park()
449
+ end
450
+ self:lose(session, "authority")
451
+ end
452
+ port.delay(session.safeUntil - bootClock(), check)
453
+ end
454
+ function ProfileRuntime:armRenewal(session)
455
+ local port = self.engine.port
456
+ local tick
457
+ tick = function()
458
+ if session.phase ~= 1 or self.engine.settled then
459
+ return nil
460
+ end
461
+ local now = bootClock()
462
+ if now >= session.renewAt then
463
+ if not self:present(session.player) then
464
+ self:leave(session.player)
465
+ return nil
466
+ end
467
+ self:requestWrite(session, 3)
468
+ end
469
+ port.delay(math.max(session.renewAt - bootClock(), 1), tick)
470
+ end
471
+ port.delay(math.max(session.renewAt - bootClock(), 0), tick)
472
+ end
473
+ function ProfileRuntime:seal(session, reason)
474
+ if session.phase == 0 then
475
+ session.abandoned = true
476
+ end
477
+ if session.phase ~= 1 then
478
+ return nil
479
+ end
480
+ session.phase = 2
481
+ session.sealedBy = reason
482
+ self.hooks.publish(session.player, nil)
483
+ local _sealed = self.sealed
484
+ local _userId = session.userId
485
+ local tokens = _sealed[_userId]
486
+ if tokens == nil then
487
+ tokens = {}
488
+ local _sealed_1 = self.sealed
489
+ local _userId_1 = session.userId
490
+ local _tokens = tokens
491
+ _sealed_1[_userId_1] = _tokens
492
+ end
493
+ local _tokens = tokens
494
+ local _token = session.token
495
+ local _session_1 = session
496
+ _tokens[_token] = _session_1
497
+ if reason == "authority" or reason == "fenced" then
498
+ self.engine:diagnose(`authority:{self.schema.name}:{session.token}`, `{self.schema.name}: {session.userId}'s session lost authority ({reason}) at revision {session.revision}, confirmed {session.confirmed}`)
499
+ if self:present(session.player) then
500
+ self.engine.port.players.kick(session.player, SESSION_ENDED_MESSAGE)
501
+ end
502
+ end
503
+ self.changed:wake()
504
+ end
505
+ function ProfileRuntime:lose(session, reason)
506
+ self:seal(session, reason)
507
+ self:startRelease(session)
508
+ end
509
+ function ProfileRuntime:startRelease(session)
510
+ session.releaseWanted = true
511
+ self:pumpWriter(session)
512
+ end
513
+ function ProfileRuntime:flush(session)
514
+ self:requestWrite(session, 1)
515
+ end
516
+ function ProfileRuntime:requestWrite(session, priority)
517
+ session.want = if session.want == nil then priority else math.min(session.want, priority)
518
+ self:pumpWriter(session)
519
+ end
520
+ function ProfileRuntime:pumpWriter(session)
521
+ if session.writing ~= nil then
522
+ return nil
523
+ end
524
+ local release = session.releaseWanted and session.released == nil and session.phase == 2
525
+ if not release and (session.want == nil or session.phase ~= 1) then
526
+ return nil
527
+ end
528
+ local priority = if release then 1 else session.want
529
+ session.want = nil
530
+ local port = self.engine.port
531
+ session.op += 1
532
+ local op = session.op
533
+ local decision
534
+ local sentAt = 0
535
+ local earliest
536
+ if not release then
537
+ session.renewAt = bootClock() + RENEW_INTERVAL_SECONDS + (port.random() * 2 - 1) * RENEW_JITTER_SECONDS
538
+ end
539
+ local ticket = self.engine.scheduler:submit({
540
+ store = self.schema.name,
541
+ key = self:key(session.userId),
542
+ priority = priority,
543
+ kind = "update",
544
+ renewal = not release,
545
+ safeUntil = session.safeUntil,
546
+ dispatchDeadline = if release then session.safeUntil - LOCAL_AUTHORITY_SECONDS + LEASE_SECONDS else session.safeUntil,
547
+ bytes = jsonBytes(session.value) + 1024,
548
+ transform = function()
549
+ local snapshot = session:snapshot()
550
+ session.carrying = snapshot.revision
551
+ local input = {
552
+ userId = session.userId,
553
+ token = session.token,
554
+ epoch = session.epoch,
555
+ op = op,
556
+ snapshot = snapshot,
557
+ release = release,
558
+ wall = port.wall,
559
+ }
560
+ return function(current)
561
+ return ownedWrite(current, input, function(d)
562
+ decision = d
563
+ return decision
564
+ end)
565
+ end
566
+ end,
567
+ onDispatch = function(at)
568
+ sentAt = at
569
+ return sentAt
570
+ end,
571
+ onResult = function(outcome)
572
+ if outcome.kind == "ok" or outcome.ambiguous then
573
+ if earliest == nil then
574
+ earliest = sentAt
575
+ end
576
+ end
577
+ end,
578
+ })
579
+ session.writing = ticket
580
+ ticket:onSettled(function(settled)
581
+ return self:settleOwned(session, release, settled, decision, earliest)
582
+ end)
583
+ end
584
+ function ProfileRuntime:settleOwned(session, release, settled, decision, earliest)
585
+ local _result = session.writing
586
+ if _result ~= nil then
587
+ _result = _result.ambiguous
588
+ end
589
+ local ambiguous = _result == true
590
+ session.writing = nil
591
+ local outcome = "given up"
592
+ local lost
593
+ if settled.kind == "done" and settled.outcome.kind == "ok" and decision ~= nil then
594
+ if decision.kind == "written" or decision.kind == "already" then
595
+ outcome = "committed"
596
+ self:confirm(session, decision.envelope, earliest)
597
+ elseif decision.kind == "fenced" or decision.kind == "expired" then
598
+ outcome = "fenced"
599
+ lost = if decision.kind == "fenced" then "fenced" else "authority"
600
+ else
601
+ self.engine:diagnose(`write:{self.schema.name}:{decision.kind}`, `{self.schema.name}: {session.userId}'s owned write was refused: {decision.kind}`)
602
+ lost = "authority"
603
+ end
604
+ elseif settled.kind == "uncertain" then
605
+ session.lost = math.max(session.lost, session.carrying)
606
+ end
607
+ if release then
608
+ session.released = outcome
609
+ session.releaseAmbiguous = outcome ~= "committed" and ambiguous
610
+ session.phase = 3
611
+ self:released(session)
612
+ elseif lost ~= nil then
613
+ self:lose(session, lost)
614
+ end
615
+ self.changed:wake()
616
+ self:pumpWriter(session)
617
+ end
618
+ function ProfileRuntime:released(session)
619
+ if session.released ~= "committed" then
620
+ self.engine:diagnose(`release:{self.schema.name}:{session.token}`, `{self.schema.name}: {session.userId}'s release did not confirm ({session.released}); revisions {session.confirmed + 1} to {session.revision} unconfirmed`)
621
+ end
622
+ if session.sealedBy == "retired" and self:present(session.player) then
623
+ self.engine.port.players.kick(session.player, SESSION_ENDED_MESSAGE)
624
+ end
625
+ local _sealed = self.sealed
626
+ local _userId = session.userId
627
+ local tokens = _sealed[_userId]
628
+ self.engine.port.delay(LEASE_SECONDS + TAKEOVER_GRACE_SECONDS, function()
629
+ local _result = tokens
630
+ if _result ~= nil then
631
+ local _token = session.token
632
+ _result = _result[_token]
633
+ end
634
+ if _result ~= session then
635
+ return nil
636
+ end
637
+ local _token = session.token
638
+ tokens[_token] = nil
639
+ -- ▼ ReadonlyMap.size ▼
640
+ local _size = 0
641
+ for _ in tokens do
642
+ _size += 1
643
+ end
644
+ -- ▲ ReadonlyMap.size ▲
645
+ local _condition = _size == 0
646
+ if _condition then
647
+ local _sealed_1 = self.sealed
648
+ local _userId_1 = session.userId
649
+ _condition = _sealed_1[_userId_1] == tokens
650
+ end
651
+ if _condition then
652
+ local _sealed_1 = self.sealed
653
+ local _userId_1 = session.userId
654
+ _sealed_1[_userId_1] = nil
655
+ end
656
+ end)
657
+ self:drop(session)
658
+ end
659
+ function ProfileRuntime:confirm(session, envelope, earliest)
660
+ session.confirmed = math.max(session.confirmed, envelope.commit.revision)
661
+ local _set = {}
662
+ for _, _v in envelope.receipts do
663
+ _set[_v] = true
664
+ end
665
+ session.durable = _set
666
+ if earliest ~= nil and self:accept(session) then
667
+ session.safeUntil = math.max(session.safeUntil, earliest + LOCAL_AUTHORITY_SECONDS)
668
+ end
669
+ self.hooks.delivered(session, envelope)
670
+ self.inbox:deliver(session, envelope)
671
+ end
672
+ function ProfileRuntime:releaseClaims(session, reason)
673
+ local ok, claimed = pcall(function()
674
+ local result = self.hooks.onRelease(session, session.value, reason)
675
+ return if rawequal(result, session.value) then result else session:prepare(result)
676
+ end)
677
+ if not ok then
678
+ self.engine:diagnose(`onRelease:{self.schema.name}:{session.token}`, `{self.schema.name}: {session.userId}'s onRelease ({reason}) was skipped`)
679
+ elseif not rawequal(claimed, session.value) then
680
+ session:commit(claimed)
681
+ end
682
+ end
683
+ function ProfileRuntime:release(session, reason)
684
+ while session.busy do
685
+ self.changed:park()
686
+ end
687
+ if session.phase == 0 then
688
+ session.abandoned = true
689
+ self.changed:wake()
690
+ return nil
691
+ end
692
+ if session.phase == 1 then
693
+ self:releaseClaims(session, reason)
694
+ self:seal(session, reason)
695
+ self:startRelease(session)
696
+ elseif session.phase == 3 then
697
+ self:drop(session)
698
+ end
699
+ end
700
+ function ProfileRuntime:leave(player)
701
+ local _sessions = self.sessions
702
+ local _player = player
703
+ local session = _sessions[_player]
704
+ if session ~= nil then
705
+ self:release(session, "leave")
706
+ end
707
+ self.changed:wake()
708
+ end
709
+ function ProfileRuntime:handoff(player)
710
+ local _sessions = self.sessions
711
+ local _player = player
712
+ local session = _sessions[_player]
713
+ local _result = session
714
+ if _result ~= nil then
715
+ _result:assertIdle()
716
+ end
717
+ assertOutsideCallback()
718
+ if session == nil or not self:accept(session) then
719
+ return "unavailable"
720
+ end
721
+ self:seal(session, "handoff")
722
+ self:releaseClaims(session, "handoff")
723
+ self:startRelease(session)
724
+ local deadline = bootClock() + HANDOFF_TIMEOUT_SECONDS
725
+ while session.released == nil and bootClock() < deadline and not self.engine.settled do
726
+ self.changed:park(deadline)
727
+ end
728
+ if session.released == "committed" then
729
+ return "ok"
730
+ end
731
+ local pending = session.writing
732
+ local _condition = session.releaseAmbiguous
733
+ if not _condition then
734
+ local _result_1 = pending
735
+ if _result_1 ~= nil then
736
+ _result_1 = _result_1.dispatched
737
+ end
738
+ local _condition_1 = _result_1 == true
739
+ if _condition_1 then
740
+ _condition_1 = (pending.inFlight or pending.ambiguous)
741
+ end
742
+ _condition = _condition_1
743
+ end
744
+ return if _condition then "uncertain" else "timeout"
745
+ end
746
+ function ProfileRuntime:resume(player)
747
+ local _sessions = self.sessions
748
+ local _player = player
749
+ local session = _sessions[_player]
750
+ local _result = session
751
+ if _result ~= nil then
752
+ _result:assertIdle()
753
+ end
754
+ assertOutsideCallback()
755
+ if session ~= nil and self:accept(session) then
756
+ return true
757
+ end
758
+ if not self:present(player) then
759
+ return false
760
+ end
761
+ return self:load(player, "resume")
762
+ end
763
+ function ProfileRuntime:await(player)
764
+ local deadline = bootClock() + LOAD_DEADLINE_SECONDS
765
+ while true do
766
+ local _sessions = self.sessions
767
+ local _player = player
768
+ local session = _sessions[_player]
769
+ if session ~= nil and self:accept(session) then
770
+ return true
771
+ end
772
+ if not self:present(player) or self.engine.closing or bootClock() >= deadline then
773
+ return false
774
+ end
775
+ if session ~= nil and session.phase ~= 0 then
776
+ return false
777
+ end
778
+ self.changed:park(deadline)
779
+ end
780
+ end
781
+ function ProfileRuntime:save(player, timeoutSeconds)
782
+ if timeoutSeconds == nil then
783
+ timeoutSeconds = SAVE_TIMEOUT_DEFAULT_SECONDS
784
+ end
785
+ assertOutsideCallback()
786
+ local _sessions = self.sessions
787
+ local _player = player
788
+ local session = _sessions[_player]
789
+ if session == nil or not self:accept(session) then
790
+ return "unavailable"
791
+ end
792
+ local deadline = bootClock() + math.clamp(timeoutSeconds, 0, SAVE_TIMEOUT_MAX_SECONDS)
793
+ local target = session.revision
794
+ self:requestWrite(session, 3)
795
+ while session.confirmed < target and session.phase ~= 3 and bootClock() < deadline and not self.engine.settled do
796
+ self.changed:park(deadline)
797
+ end
798
+ if session.confirmed >= target then
799
+ return "ok"
800
+ end
801
+ return if session:uncertain() >= target then "uncertain" else "timeout"
802
+ end
803
+ function ProfileRuntime:inspect(userId)
804
+ for _, session in self.sessions do
805
+ if session.userId == userId and self:accept(session) then
806
+ return {
807
+ kind = "ok",
808
+ value = session.value,
809
+ }
810
+ end
811
+ end
812
+ local deadline = bootClock() + CALL_DEADLINE_SECONDS
813
+ local ticket = self.engine.scheduler:submit({
814
+ store = self.schema.name,
815
+ key = self:key(userId),
816
+ priority = 5,
817
+ kind = "read",
818
+ dispatchDeadline = deadline,
819
+ bytes = 1024,
820
+ })
821
+ local settled = ticket:wait(deadline) or {
822
+ kind = if ticket.dispatched then "uncertain" else "timeout",
823
+ }
824
+ if settled.kind ~= "done" then
825
+ return {
826
+ kind = settled.kind,
827
+ }
828
+ end
829
+ if settled.outcome.kind == "error" then
830
+ return {
831
+ kind = "unavailable",
832
+ }
833
+ end
834
+ local _result = settled.outcome.value
835
+ if _result ~= nil then
836
+ _result = _result.value
837
+ end
838
+ local parsed = parseEnvelope(_result, "profiles")
839
+ if parsed.kind == "absent" or (parsed.kind == "ok" and parsed.envelope.payload == nil) then
840
+ return {
841
+ kind = "ok",
842
+ value = nil,
843
+ }
844
+ end
845
+ if parsed.kind == "unreadable" then
846
+ return {
847
+ kind = "unavailable",
848
+ }
849
+ end
850
+ local loaded = loadRecord(self.schema, parsed.envelope.payload, parsed.envelope.schemaVersion)
851
+ return if loaded.kind == "ok" then {
852
+ kind = "ok",
853
+ value = loaded.value,
854
+ } else {
855
+ kind = "unavailable",
856
+ }
857
+ end
858
+ end
859
+ return {
860
+ DataEngine = DataEngine,
861
+ ProfileRuntime = ProfileRuntime,
862
+ }