@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.
- package/maths/spline/package.json +4 -0
- package/out/client/index.d.ts +7 -0
- package/out/client/init.luau +36 -1
- package/out/configs.d.ts +37 -0
- package/out/configs.luau +46 -0
- package/out/data.d.ts +224 -0
- package/out/data.luau +97 -0
- package/out/emit/configs.d.ts +4 -0
- package/out/emit/configs.luau +22 -0
- package/out/emit/data.d.ts +6 -0
- package/out/emit/data.luau +19 -0
- package/out/emit/index.d.ts +5 -1
- package/out/emit/init.luau +23 -7
- package/out/emit/network.d.ts +6 -0
- package/out/emit/network.luau +36 -0
- package/out/emit/replicated.d.ts +4 -0
- package/out/emit/replicated.luau +13 -0
- package/out/index.d.ts +8 -0
- package/out/init.luau +23 -0
- package/out/internal/binding.luau +5 -0
- package/out/internal/boot.luau +112 -8
- package/out/internal/configs/limits.d.ts +10 -0
- package/out/internal/configs/limits.luau +25 -0
- package/out/internal/configs/port.d.ts +29 -0
- package/out/internal/configs/port.luau +99 -0
- package/out/internal/configs/runtime.d.ts +21 -0
- package/out/internal/configs/runtime.luau +981 -0
- package/out/internal/data/admission.d.ts +1 -0
- package/out/internal/data/admission.luau +673 -0
- package/out/internal/data/declarations.d.ts +1 -0
- package/out/internal/data/declarations.luau +384 -0
- package/out/internal/data/envelope.d.ts +1 -0
- package/out/internal/data/envelope.luau +318 -0
- package/out/internal/data/handles.d.ts +1 -0
- package/out/internal/data/handles.luau +255 -0
- package/out/internal/data/inbox-transforms.d.ts +1 -0
- package/out/internal/data/inbox-transforms.luau +458 -0
- package/out/internal/data/inbox.d.ts +1 -0
- package/out/internal/data/inbox.luau +437 -0
- package/out/internal/data/lease.d.ts +1 -0
- package/out/internal/data/lease.luau +304 -0
- package/out/internal/data/limits.d.ts +1 -0
- package/out/internal/data/limits.luau +234 -0
- package/out/internal/data/messages.d.ts +1 -0
- package/out/internal/data/messages.luau +69 -0
- package/out/internal/data/operation-id.d.ts +1 -0
- package/out/internal/data/operation-id.luau +61 -0
- package/out/internal/data/paths.d.ts +2 -0
- package/out/internal/data/paths.luau +323 -0
- package/out/internal/data/port.d.ts +1 -0
- package/out/internal/data/port.luau +290 -0
- package/out/internal/data/profiles.d.ts +1 -0
- package/out/internal/data/profiles.luau +862 -0
- package/out/internal/data/receipts.d.ts +1 -0
- package/out/internal/data/receipts.luau +190 -0
- package/out/internal/data/record-bindings.d.ts +1 -0
- package/out/internal/data/record-bindings.luau +92 -0
- package/out/internal/data/record-declarations.d.ts +1 -0
- package/out/internal/data/record-declarations.luau +140 -0
- package/out/internal/data/records.d.ts +1 -0
- package/out/internal/data/records.luau +400 -0
- package/out/internal/data/runtime.d.ts +1 -0
- package/out/internal/data/runtime.luau +918 -0
- package/out/internal/data/scheduler.d.ts +1 -0
- package/out/internal/data/scheduler.luau +507 -0
- package/out/internal/data/session.d.ts +1 -0
- package/out/internal/data/session.luau +234 -0
- package/out/internal/data/shutdown.d.ts +1 -0
- package/out/internal/data/shutdown.luau +91 -0
- package/out/internal/data/waiters.d.ts +1 -0
- package/out/internal/data/waiters.luau +65 -0
- package/out/internal/lifecycle.luau +45 -0
- package/out/internal/network/catalog.d.ts +26 -0
- package/out/internal/network/catalog.luau +182 -0
- package/out/internal/network/claims.d.ts +1 -0
- package/out/internal/network/claims.luau +146 -0
- package/out/internal/network/client-session.d.ts +1 -0
- package/out/internal/network/client-session.luau +405 -0
- package/out/internal/network/codec.d.ts +63 -0
- package/out/internal/network/codec.luau +1183 -0
- package/out/internal/network/envelope.d.ts +1 -0
- package/out/internal/network/envelope.luau +247 -0
- package/out/internal/network/limits.d.ts +26 -0
- package/out/internal/network/limits.luau +55 -0
- package/out/internal/network/port.d.ts +1 -0
- package/out/internal/network/port.luau +19 -0
- package/out/internal/network/roblox-network-port.d.ts +1 -0
- package/out/internal/network/roblox-network-port.luau +105 -0
- package/out/internal/network/schema.d.ts +72 -0
- package/out/internal/network/schema.luau +386 -0
- package/out/internal/network/server-session.d.ts +1 -0
- package/out/internal/network/server-session.luau +395 -0
- package/out/internal/network/session.d.ts +1 -0
- package/out/internal/network/session.luau +266 -0
- package/out/internal/reactive/context.d.ts +1 -0
- package/out/internal/reactive/context.luau +149 -0
- package/out/internal/reactive/flush.d.ts +1 -0
- package/out/internal/reactive/flush.luau +355 -0
- package/out/internal/reactive/graph.d.ts +1 -0
- package/out/internal/reactive/graph.luau +626 -0
- package/out/internal/reactive/limits.d.ts +1 -0
- package/out/internal/reactive/limits.luau +54 -0
- package/out/internal/reactive/ownership.d.ts +1 -0
- package/out/internal/reactive/ownership.luau +626 -0
- package/out/internal/registry.luau +10 -1
- package/out/internal/sync/client-sync.d.ts +1 -0
- package/out/internal/sync/client-sync.luau +157 -0
- package/out/internal/sync/declarations.d.ts +1 -0
- package/out/internal/sync/declarations.luau +276 -0
- package/out/internal/sync/frame.d.ts +1 -0
- package/out/internal/sync/frame.luau +91 -0
- package/out/internal/sync/server-sync.d.ts +1 -0
- package/out/internal/sync/server-sync.luau +502 -0
- package/out/internal/sync/values.d.ts +1 -0
- package/out/internal/sync/values.luau +930 -0
- package/out/maths/spline/curve.d.ts +35 -0
- package/out/maths/spline/curve.luau +1122 -0
- package/out/maths/spline/index.d.ts +2 -0
- package/out/maths/spline/init.luau +8 -0
- package/out/network.d.ts +66 -0
- package/out/network.luau +24 -0
- package/out/reactive.d.ts +76 -0
- package/out/reactive.luau +136 -0
- package/out/replicated.d.ts +38 -0
- package/out/replicated.luau +45 -0
- package/out/server/data.d.ts +54 -0
- package/out/server/data.luau +44 -0
- package/out/server/index.d.ts +17 -0
- package/out/server/init.luau +58 -4
- package/out/server/publish.d.ts +20 -0
- package/out/server/publish.luau +24 -0
- package/out/testing/index.d.ts +38 -0
- package/out/testing/init.luau +189 -0
- package/out/util/random/index.d.ts +24 -0
- package/out/util/random/init.luau +301 -0
- package/out/vide/index.d.ts +7 -0
- package/out/vide/init.luau +50 -0
- package/package.json +28 -2
- package/testing/package.json +4 -0
- package/transformer/data.cjs +72 -0
- package/transformer/declarations.cjs +153 -0
- package/transformer/index.js +87 -23
- package/transformer/network.cjs +961 -0
- package/util/random/package.json +4 -0
- package/vide/package.json +4 -0
|
@@ -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
|
+
}
|