@quenty/datastore 13.51.1 → 13.52.1
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/CHANGELOG.md +19 -0
- package/package.json +17 -16
- package/src/Client/Cmdr/DataStoreCmdrServiceClient.lua +48 -0
- package/src/Client/DataStoreServiceClient.lua +35 -0
- package/src/Server/Cmdr/DataStoreCmdrService.lua +454 -0
- package/src/Server/Cmdr/DataStoreCmdrService.spec.lua +370 -0
- package/src/Server/DataStore.lua +21 -0
- package/src/Server/DataStoreLockHelper.lua +8 -86
- package/src/Server/DataStoreService.lua +40 -0
- package/src/Server/Modules/DataStoreLockUtils.lua +139 -0
- package/src/Server/PlayerDataStoreHandle.lua +103 -0
- package/src/Server/PlayerDataStoreManager.Handles.spec.lua +136 -0
- package/src/Server/PlayerDataStoreManager.SessionLockTools.spec.lua +228 -0
- package/src/Server/PlayerDataStoreManager.lua +282 -0
- package/src/Server/PlayerDataStoreManager.spec.lua +86 -0
- package/src/Server/PlayerDataStoreService.lua +112 -0
- package/src/Server/PlayerDataStoreService.spec.lua +65 -0
- package/src/Shared/Cmdr/DataStoreCmdrUtils.lua +57 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Drives the command bodies directly. The service is built by hand rather than through a ServiceBag
|
|
4
|
+
so the real CmdrService (and the Cmdr instance tree behind it) stays out of the test place --
|
|
5
|
+
what is worth checking here is what the commands say and what they write, not Cmdr's plumbing.
|
|
6
|
+
|
|
7
|
+
Command bodies take the userId list Cmdr's `playerIds` type parses, so these pass one directly.
|
|
8
|
+
|
|
9
|
+
@class DataStoreCmdrService.spec.lua
|
|
10
|
+
]]
|
|
11
|
+
local require = require(script.Parent.loader).load(script)
|
|
12
|
+
|
|
13
|
+
local HttpService = game:GetService("HttpService")
|
|
14
|
+
|
|
15
|
+
local CmdrReplyUtils = require("CmdrReplyUtils")
|
|
16
|
+
local DataStoreCmdrService = require("DataStoreCmdrService")
|
|
17
|
+
local DataStoreTestUtils = require("DataStoreTestUtils")
|
|
18
|
+
local Jest = require("Jest")
|
|
19
|
+
local Maid = require("Maid")
|
|
20
|
+
local Promise = require("Promise")
|
|
21
|
+
|
|
22
|
+
local describe = Jest.Globals.describe
|
|
23
|
+
local expect = Jest.Globals.expect
|
|
24
|
+
local it = Jest.Globals.it
|
|
25
|
+
|
|
26
|
+
-- Short enough that the progress tests do not have to sit through the real threshold.
|
|
27
|
+
local SLOW_REPLY_SECONDS = 0.05
|
|
28
|
+
|
|
29
|
+
local FOREIGN_LOCK = {
|
|
30
|
+
LastUpdateTime = os.time(),
|
|
31
|
+
ActiveSession = {
|
|
32
|
+
SessionId = "foreign-session",
|
|
33
|
+
PlaceId = 123,
|
|
34
|
+
JobId = "foreign-job",
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
local function setup()
|
|
39
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
40
|
+
|
|
41
|
+
local registered = {}
|
|
42
|
+
local registeredTypes = {}
|
|
43
|
+
local cmdr = {
|
|
44
|
+
Registry = {
|
|
45
|
+
RegisterType = function(_self, name, definition)
|
|
46
|
+
registeredTypes[name] = definition
|
|
47
|
+
end,
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
local cmdrService = {
|
|
51
|
+
RegisterCommand = function(_self, definition, execute)
|
|
52
|
+
-- CmdrService ships the definition to the client as JSON, so a definition it cannot encode
|
|
53
|
+
-- fails in-game at registration. Encode it here, where the failure is a test result.
|
|
54
|
+
HttpService:JSONEncode(definition)
|
|
55
|
+
registered[definition.Name] = execute
|
|
56
|
+
end,
|
|
57
|
+
PromiseCmdr = function()
|
|
58
|
+
return Promise.resolved(cmdr)
|
|
59
|
+
end,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
local replies: { string } = {}
|
|
63
|
+
local context = {
|
|
64
|
+
Reply = function(_self, text: string)
|
|
65
|
+
table.insert(replies, text)
|
|
66
|
+
end,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
local serviceMaid = Maid.new()
|
|
70
|
+
local service = setmetatable({
|
|
71
|
+
_maid = serviceMaid,
|
|
72
|
+
_cmdrService = cmdrService,
|
|
73
|
+
_playerDataStoreService = {
|
|
74
|
+
PromiseManager = function()
|
|
75
|
+
return Promise.resolved(controller.manager)
|
|
76
|
+
end,
|
|
77
|
+
},
|
|
78
|
+
}, { __index = DataStoreCmdrService }) :: any
|
|
79
|
+
|
|
80
|
+
service:SetReplyConfig(CmdrReplyUtils.createConfig({ slowReplySeconds = SLOW_REPLY_SECONDS }))
|
|
81
|
+
service:Start()
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
manager = controller.manager,
|
|
85
|
+
mock = controller.mock,
|
|
86
|
+
storeAndAwaitLock = controller.storeAndAwaitLock,
|
|
87
|
+
subStoreType = function()
|
|
88
|
+
return registeredTypes.dataStoreSubStore
|
|
89
|
+
end,
|
|
90
|
+
run = function(commandName: string, ...)
|
|
91
|
+
return registered[commandName](context, ...)
|
|
92
|
+
end,
|
|
93
|
+
replies = replies,
|
|
94
|
+
registeredNames = function()
|
|
95
|
+
local names = {}
|
|
96
|
+
for name, _ in registered do
|
|
97
|
+
table.insert(names, name)
|
|
98
|
+
end
|
|
99
|
+
table.sort(names)
|
|
100
|
+
return names
|
|
101
|
+
end,
|
|
102
|
+
destroy = function(_self)
|
|
103
|
+
serviceMaid:DoCleaning()
|
|
104
|
+
controller:destroy()
|
|
105
|
+
end,
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
describe("DataStoreCmdrService registration", function()
|
|
110
|
+
it("registers the lock and data commands", function()
|
|
111
|
+
local controller = setup()
|
|
112
|
+
|
|
113
|
+
expect(controller.registeredNames()).toEqual({
|
|
114
|
+
"datastore-copy",
|
|
115
|
+
"datastore-delete",
|
|
116
|
+
"datastore-lock",
|
|
117
|
+
"datastore-lock-info",
|
|
118
|
+
"datastore-read-json",
|
|
119
|
+
"datastore-unlock",
|
|
120
|
+
"datastore-write-json",
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
controller:destroy()
|
|
124
|
+
end)
|
|
125
|
+
|
|
126
|
+
it("registers the sub-store type", function()
|
|
127
|
+
local controller = setup()
|
|
128
|
+
|
|
129
|
+
expect(controller.subStoreType()).never.toBeNil()
|
|
130
|
+
|
|
131
|
+
controller:destroy()
|
|
132
|
+
end)
|
|
133
|
+
|
|
134
|
+
it("takes players in bulk", function()
|
|
135
|
+
local controller = setup()
|
|
136
|
+
|
|
137
|
+
expect(controller.run("datastore-lock-info", {})).toEqual("No players to act on.")
|
|
138
|
+
|
|
139
|
+
controller:destroy()
|
|
140
|
+
end)
|
|
141
|
+
|
|
142
|
+
it("stays quiet while a command is quick", function()
|
|
143
|
+
local controller = setup()
|
|
144
|
+
|
|
145
|
+
controller.run("datastore-lock-info", { 1 })
|
|
146
|
+
task.wait(SLOW_REPLY_SECONDS * 2)
|
|
147
|
+
|
|
148
|
+
expect(#controller.replies).toEqual(0)
|
|
149
|
+
|
|
150
|
+
controller:destroy()
|
|
151
|
+
end)
|
|
152
|
+
|
|
153
|
+
it("reports a target that is taking a while", function()
|
|
154
|
+
local controller = setup()
|
|
155
|
+
|
|
156
|
+
controller.mock:SetYieldTime(SLOW_REPLY_SECONDS * 2)
|
|
157
|
+
|
|
158
|
+
controller.run("datastore-lock-info", { 1 })
|
|
159
|
+
|
|
160
|
+
expect(#controller.replies).toEqual(1)
|
|
161
|
+
expect(string.find(controller.replies[1], "1: ", 1, true) ~= nil).toEqual(true)
|
|
162
|
+
|
|
163
|
+
controller:destroy()
|
|
164
|
+
end)
|
|
165
|
+
end)
|
|
166
|
+
|
|
167
|
+
describe("datastore-lock-info", function()
|
|
168
|
+
it("reports an unlocked key", function()
|
|
169
|
+
local controller = setup()
|
|
170
|
+
|
|
171
|
+
expect(string.find(controller.run("datastore-lock-info", { 1 }), "unlocked") ~= nil).toEqual(true)
|
|
172
|
+
|
|
173
|
+
controller:destroy()
|
|
174
|
+
end)
|
|
175
|
+
|
|
176
|
+
it("names the session holding the lock", function()
|
|
177
|
+
local controller = setup()
|
|
178
|
+
|
|
179
|
+
controller.mock:SetRaw("user_1", { coins = 5, lock = FOREIGN_LOCK })
|
|
180
|
+
|
|
181
|
+
expect(string.find(controller.run("datastore-lock-info", { 1 }), "foreign%-job") ~= nil).toEqual(true)
|
|
182
|
+
|
|
183
|
+
controller:destroy()
|
|
184
|
+
end)
|
|
185
|
+
|
|
186
|
+
it("reports every target on its own line", function()
|
|
187
|
+
local controller = setup()
|
|
188
|
+
|
|
189
|
+
controller.mock:SetRaw("user_1", { coins = 5, lock = FOREIGN_LOCK })
|
|
190
|
+
controller.mock:SetRaw("user_2", { coins = 5 })
|
|
191
|
+
|
|
192
|
+
local output = controller.run("datastore-lock-info", { 1, 2 })
|
|
193
|
+
expect(string.find(output, "1: ") ~= nil).toEqual(true)
|
|
194
|
+
expect(string.find(output, "2: ") ~= nil).toEqual(true)
|
|
195
|
+
|
|
196
|
+
controller:destroy()
|
|
197
|
+
end)
|
|
198
|
+
end)
|
|
199
|
+
|
|
200
|
+
describe("datastore-unlock", function()
|
|
201
|
+
it("clears a foreign lock", function()
|
|
202
|
+
local controller = setup()
|
|
203
|
+
|
|
204
|
+
controller.mock:SetRaw("user_1", { coins = 5, lock = FOREIGN_LOCK })
|
|
205
|
+
|
|
206
|
+
local output = controller.run("datastore-unlock", { 1 })
|
|
207
|
+
expect(string.find(output, "Unlocked") ~= nil).toEqual(true)
|
|
208
|
+
expect(controller.mock:GetRaw("user_1").lock).toBeNil()
|
|
209
|
+
expect(controller.mock:GetRaw("user_1").coins).toEqual(5)
|
|
210
|
+
|
|
211
|
+
controller:destroy()
|
|
212
|
+
end)
|
|
213
|
+
|
|
214
|
+
it("says so when there was nothing to clear", function()
|
|
215
|
+
local controller = setup()
|
|
216
|
+
|
|
217
|
+
controller.mock:SetRaw("user_1", { coins = 5 })
|
|
218
|
+
|
|
219
|
+
expect(string.find(controller.run("datastore-unlock", { 1 }), "already unlocked") ~= nil).toEqual(true)
|
|
220
|
+
|
|
221
|
+
controller:destroy()
|
|
222
|
+
end)
|
|
223
|
+
|
|
224
|
+
-- These are stress-test tools, so a live local session is a target rather than a refusal.
|
|
225
|
+
it("clears a key this server holds a live session for", function()
|
|
226
|
+
local controller = setup()
|
|
227
|
+
|
|
228
|
+
if not controller.storeAndAwaitLock() then
|
|
229
|
+
expect("lock was never acquired").toEqual("lock was acquired")
|
|
230
|
+
controller:destroy()
|
|
231
|
+
return
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
expect(string.find(controller.run("datastore-unlock", { 1 }), "Unlocked") ~= nil).toEqual(true)
|
|
235
|
+
expect(controller.mock:GetRaw("user_1").lock).toBeNil()
|
|
236
|
+
|
|
237
|
+
controller:destroy()
|
|
238
|
+
end)
|
|
239
|
+
|
|
240
|
+
it("clears every target in a batch", function()
|
|
241
|
+
local controller = setup()
|
|
242
|
+
|
|
243
|
+
controller.mock:SetRaw("user_1", { coins = 5, lock = FOREIGN_LOCK })
|
|
244
|
+
controller.mock:SetRaw("user_2", { coins = 5, lock = FOREIGN_LOCK })
|
|
245
|
+
|
|
246
|
+
local output = controller.run("datastore-unlock", { 1, 2 })
|
|
247
|
+
expect(string.find(output, "Unlocked 1") ~= nil).toEqual(true)
|
|
248
|
+
expect(string.find(output, "Unlocked 2") ~= nil).toEqual(true)
|
|
249
|
+
expect(controller.mock:GetRaw("user_1").lock).toBeNil()
|
|
250
|
+
expect(controller.mock:GetRaw("user_2").lock).toBeNil()
|
|
251
|
+
|
|
252
|
+
controller:destroy()
|
|
253
|
+
end)
|
|
254
|
+
end)
|
|
255
|
+
|
|
256
|
+
describe("datastore-lock", function()
|
|
257
|
+
it("claims an unlocked key", function()
|
|
258
|
+
local controller = setup()
|
|
259
|
+
|
|
260
|
+
controller.mock:SetRaw("user_1", { coins = 5 })
|
|
261
|
+
|
|
262
|
+
expect(string.find(controller.run("datastore-lock", { 1 }), "Locked") ~= nil).toEqual(true)
|
|
263
|
+
expect(controller.mock:GetRaw("user_1").lock).never.toBeNil()
|
|
264
|
+
|
|
265
|
+
controller:destroy()
|
|
266
|
+
end)
|
|
267
|
+
|
|
268
|
+
it("reports the lock it replaced", function()
|
|
269
|
+
local controller = setup()
|
|
270
|
+
|
|
271
|
+
controller.mock:SetRaw("user_1", { coins = 5, lock = FOREIGN_LOCK })
|
|
272
|
+
|
|
273
|
+
expect(string.find(controller.run("datastore-lock", { 1 }), "foreign%-job") ~= nil).toEqual(true)
|
|
274
|
+
|
|
275
|
+
controller:destroy()
|
|
276
|
+
end)
|
|
277
|
+
end)
|
|
278
|
+
|
|
279
|
+
-- The data commands scope to a sub-store rather than the root wherever they can, so a write under
|
|
280
|
+
-- test cannot clobber the session lock the manager is keeping on the same key.
|
|
281
|
+
describe("datastore-read-json", function()
|
|
282
|
+
it("reads the stored data back as JSON", function()
|
|
283
|
+
local controller = setup()
|
|
284
|
+
|
|
285
|
+
controller.mock:SetRaw("user_1", { profile = { level = 7 } })
|
|
286
|
+
|
|
287
|
+
local output = controller.run("datastore-read-json", { 1 }, { "profile" })
|
|
288
|
+
expect(string.find(output, '"level"') ~= nil).toEqual(true)
|
|
289
|
+
expect(string.find(output, "7") ~= nil).toEqual(true)
|
|
290
|
+
|
|
291
|
+
controller:destroy()
|
|
292
|
+
end)
|
|
293
|
+
end)
|
|
294
|
+
|
|
295
|
+
describe("datastore-write-json", function()
|
|
296
|
+
it("writes a sub-store", function()
|
|
297
|
+
local controller = setup()
|
|
298
|
+
|
|
299
|
+
controller.mock:SetRaw("user_1", { coins = 5 })
|
|
300
|
+
|
|
301
|
+
controller.run("datastore-write-json", { 1 }, '{"level":7}', { "profile" })
|
|
302
|
+
expect(controller.mock:GetRaw("user_1").profile.level).toEqual(7)
|
|
303
|
+
expect(controller.mock:GetRaw("user_1").coins).toEqual(5)
|
|
304
|
+
|
|
305
|
+
controller:destroy()
|
|
306
|
+
end)
|
|
307
|
+
|
|
308
|
+
it("reports undecodable JSON as a failure", function()
|
|
309
|
+
local controller = setup()
|
|
310
|
+
|
|
311
|
+
expect(string.find(controller.run("datastore-write-json", { 1 }, "not json", nil), "Failed") ~= nil).toEqual(
|
|
312
|
+
true
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
controller:destroy()
|
|
316
|
+
end)
|
|
317
|
+
end)
|
|
318
|
+
|
|
319
|
+
describe("datastore-delete", function()
|
|
320
|
+
it("wipes a sub-store and leaves the rest of the key", function()
|
|
321
|
+
local controller = setup()
|
|
322
|
+
|
|
323
|
+
controller.mock:SetRaw("user_1", { coins = 5, profile = { level = 7 } })
|
|
324
|
+
|
|
325
|
+
controller.run("datastore-delete", { 1 }, { "profile" })
|
|
326
|
+
expect(controller.mock:GetRaw("user_1").profile).toBeNil()
|
|
327
|
+
expect(controller.mock:GetRaw("user_1").coins).toEqual(5)
|
|
328
|
+
|
|
329
|
+
controller:destroy()
|
|
330
|
+
end)
|
|
331
|
+
end)
|
|
332
|
+
|
|
333
|
+
describe("datastore-copy", function()
|
|
334
|
+
it("copies a sub-store onto another player", function()
|
|
335
|
+
local controller = setup()
|
|
336
|
+
|
|
337
|
+
controller.mock:SetRaw("user_1", { profile = { level = 7 } })
|
|
338
|
+
controller.mock:SetRaw("user_2", { profile = { level = 1 } })
|
|
339
|
+
|
|
340
|
+
controller.run("datastore-copy", 1, { 2 }, { "profile" })
|
|
341
|
+
expect(controller.mock:GetRaw("user_2").profile.level).toEqual(7)
|
|
342
|
+
|
|
343
|
+
controller:destroy()
|
|
344
|
+
end)
|
|
345
|
+
|
|
346
|
+
it("copies onto an absent player through a store that yields", function()
|
|
347
|
+
local controller = setup()
|
|
348
|
+
|
|
349
|
+
controller.mock:SetRaw("user_1", { profile = { level = 7 } })
|
|
350
|
+
controller.mock:SetRaw("user_2", { profile = { level = 1 } })
|
|
351
|
+
controller.mock:SetYieldTime(0.05)
|
|
352
|
+
|
|
353
|
+
controller.run("datastore-copy", 1, { 2 }, { "profile" })
|
|
354
|
+
|
|
355
|
+
expect(controller.mock:GetRaw("user_2").profile.level).toEqual(7)
|
|
356
|
+
|
|
357
|
+
controller:destroy()
|
|
358
|
+
end)
|
|
359
|
+
|
|
360
|
+
it("skips the source when it is also a target", function()
|
|
361
|
+
local controller = setup()
|
|
362
|
+
|
|
363
|
+
controller.mock:SetRaw("user_1", { profile = { level = 7 } })
|
|
364
|
+
|
|
365
|
+
local output = controller.run("datastore-copy", 1, { 1 }, { "profile" })
|
|
366
|
+
expect(string.find(output, "Skipped") ~= nil).toEqual(true)
|
|
367
|
+
|
|
368
|
+
controller:destroy()
|
|
369
|
+
end)
|
|
370
|
+
end)
|
package/src/Server/DataStore.lua
CHANGED
|
@@ -345,6 +345,14 @@ function DataStore.SetLoadRetryOptions(self: DataStore, options: PromiseRetryUti
|
|
|
345
345
|
self._loadRetryOptions = options
|
|
346
346
|
end
|
|
347
347
|
|
|
348
|
+
--[=[
|
|
349
|
+
Returns the load retry backoff in use.
|
|
350
|
+
@return RetryOptions
|
|
351
|
+
]=]
|
|
352
|
+
function DataStore.GetLoadRetryOptions(self: DataStore): PromiseRetryUtils.RetryOptions
|
|
353
|
+
return self._loadRetryOptions
|
|
354
|
+
end
|
|
355
|
+
|
|
348
356
|
--[=[
|
|
349
357
|
Overrides how long to wait for Roblox to replicate a released lock after a graceful session-close
|
|
350
358
|
request before retrying the load. Defaults to 5 seconds. Mainly useful for tests.
|
|
@@ -357,6 +365,14 @@ function DataStore.SetSessionMessagingCloseDelaySeconds(self: DataStore, seconds
|
|
|
357
365
|
self._sessionMessagingCloseDelaySeconds = seconds
|
|
358
366
|
end
|
|
359
367
|
|
|
368
|
+
--[=[
|
|
369
|
+
Returns the post-graceful-close replication delay in use.
|
|
370
|
+
@return number
|
|
371
|
+
]=]
|
|
372
|
+
function DataStore.GetSessionMessagingCloseDelaySeconds(self: DataStore): number
|
|
373
|
+
return self._sessionMessagingCloseDelaySeconds
|
|
374
|
+
end
|
|
375
|
+
|
|
360
376
|
--[=[
|
|
361
377
|
Returns whether the datastore failed.
|
|
362
378
|
@return boolean
|
|
@@ -693,6 +709,11 @@ function DataStore._promiseGetAsyncNoCache(self: DataStore): Promise.Promise<()>
|
|
|
693
709
|
)
|
|
694
710
|
end
|
|
695
711
|
|
|
712
|
+
-- TODO: Bail out when this store was destroyed mid-load instead of erroring here.
|
|
713
|
+
-- Releasing the last handle for an absent player destroys the lock helper while
|
|
714
|
+
-- this UpdateAsync is still outstanding, and a destroyed BaseObject has no
|
|
715
|
+
-- methods left. Roblox aborts the update on a transform error, so this is noise
|
|
716
|
+
-- rather than a lost write.
|
|
696
717
|
local lockResult = self._sessionLockingEnabledHelper:AcquireLock(data, canStealLock)
|
|
697
718
|
if not lockResult.isValid then
|
|
698
719
|
if self._sessionMessagingEnabledHelper and tryMessagingServiceSessionClose then
|
|
@@ -8,6 +8,7 @@ local RunService = game:GetService("RunService")
|
|
|
8
8
|
local require = require(script.Parent.loader).load(script)
|
|
9
9
|
|
|
10
10
|
local BaseObject = require("BaseObject")
|
|
11
|
+
local DataStoreLockUtils = require("DataStoreLockUtils")
|
|
11
12
|
local Promise = require("Promise")
|
|
12
13
|
|
|
13
14
|
local DataStoreLockHelper = setmetatable({}, BaseObject)
|
|
@@ -27,16 +28,8 @@ export type DataStoreLockHelper =
|
|
|
27
28
|
))
|
|
28
29
|
& BaseObject.BaseObject
|
|
29
30
|
|
|
30
|
-
export type LockedSessionData =
|
|
31
|
-
|
|
32
|
-
PlaceId: number,
|
|
33
|
-
JobId: string,
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export type LockData = {
|
|
37
|
-
LastUpdateTime: number?,
|
|
38
|
-
ActiveSession: LockedSessionData?,
|
|
39
|
-
}
|
|
31
|
+
export type LockedSessionData = DataStoreLockUtils.LockedSessionData
|
|
32
|
+
export type LockData = DataStoreLockUtils.LockData
|
|
40
33
|
|
|
41
34
|
export type AcquiredValidLockResult = {
|
|
42
35
|
isValid: true,
|
|
@@ -88,7 +81,7 @@ function DataStoreLockHelper.ToUnlockedProfile(self: DataStoreLockHelper, origin
|
|
|
88
81
|
unlockedProfile = original,
|
|
89
82
|
}
|
|
90
83
|
else
|
|
91
|
-
local parsedLockData =
|
|
84
|
+
local parsedLockData = DataStoreLockUtils.deserializeLockData(original.lock)
|
|
92
85
|
if parsedLockData == nil or parsedLockData.ActiveSession == nil then
|
|
93
86
|
return {
|
|
94
87
|
isValid = true,
|
|
@@ -113,50 +106,16 @@ function DataStoreLockHelper.ToUnlockedProfile(self: DataStoreLockHelper, origin
|
|
|
113
106
|
end
|
|
114
107
|
|
|
115
108
|
function DataStoreLockHelper.ToRawUnlockedProfile(_self: DataStoreLockHelper, original: any): any
|
|
116
|
-
|
|
117
|
-
return {}
|
|
118
|
-
elseif type(original) ~= "table" then
|
|
119
|
-
warn("[DataStoreLockHelper] - Data session locking is not available for non-table entries")
|
|
120
|
-
return original
|
|
121
|
-
else
|
|
122
|
-
local copy = table.clone(original)
|
|
123
|
-
copy.lock = nil
|
|
124
|
-
return copy
|
|
125
|
-
end
|
|
109
|
+
return DataStoreLockUtils.withLock(original, nil)
|
|
126
110
|
end
|
|
127
111
|
|
|
128
112
|
function DataStoreLockHelper.ToLockedProfile(self: DataStoreLockHelper, original: any, doCloseSession: boolean?): any
|
|
129
113
|
if doCloseSession then
|
|
130
114
|
self._sessionClosedPromise:Resolve()
|
|
115
|
+
return DataStoreLockUtils.withLock(original, nil)
|
|
131
116
|
end
|
|
132
117
|
|
|
133
|
-
|
|
134
|
-
if doCloseSession then
|
|
135
|
-
return {}
|
|
136
|
-
else
|
|
137
|
-
return {
|
|
138
|
-
lock = {
|
|
139
|
-
LastUpdateTime = os.time(),
|
|
140
|
-
ActiveSession = self:_ourCurrentSessionData(),
|
|
141
|
-
} :: LockData,
|
|
142
|
-
}
|
|
143
|
-
end
|
|
144
|
-
elseif type(original) ~= "table" then
|
|
145
|
-
warn("[DataStoreLockHelper] - Data session locking is not available for non-table entries")
|
|
146
|
-
return original
|
|
147
|
-
else
|
|
148
|
-
local copy = table.clone(original)
|
|
149
|
-
if doCloseSession then
|
|
150
|
-
copy.lock = nil :: LockData?
|
|
151
|
-
else
|
|
152
|
-
copy.lock = {
|
|
153
|
-
LastUpdateTime = os.time(),
|
|
154
|
-
ActiveSession = self:_ourCurrentSessionData(),
|
|
155
|
-
} :: LockData
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
return copy
|
|
159
|
-
end
|
|
118
|
+
return DataStoreLockUtils.withLock(original, DataStoreLockUtils.createLockData(self:_ourCurrentSessionData()))
|
|
160
119
|
end
|
|
161
120
|
|
|
162
121
|
function DataStoreLockHelper._isInSession(self: DataStoreLockHelper, sessionData: LockedSessionData): boolean
|
|
@@ -182,43 +141,6 @@ function DataStoreLockHelper._ourCurrentSessionData(self: DataStoreLockHelper):
|
|
|
182
141
|
}
|
|
183
142
|
end
|
|
184
143
|
|
|
185
|
-
function DataStoreLockHelper._deserializeSessionData(_self: DataStoreLockHelper, sessionData: any): LockedSessionData?
|
|
186
|
-
if type(sessionData) ~= "table" then
|
|
187
|
-
return nil
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
if type(sessionData.SessionId) ~= "string" then
|
|
191
|
-
return nil
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
if type(sessionData.PlaceId) ~= "number" then
|
|
195
|
-
return nil
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
if type(sessionData.JobId) ~= "string" then
|
|
199
|
-
return nil
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
return {
|
|
203
|
-
SessionId = sessionData.SessionId,
|
|
204
|
-
PlaceId = sessionData.PlaceId,
|
|
205
|
-
JobId = sessionData.JobId,
|
|
206
|
-
}
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
function DataStoreLockHelper._deserializeLockData(self: DataStoreLockHelper, lockData: any): LockData?
|
|
210
|
-
if type(lockData) ~= "table" then
|
|
211
|
-
return nil
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
local activeSession: LockedSessionData? = self:_deserializeSessionData(lockData.ActiveSession)
|
|
215
|
-
|
|
216
|
-
return {
|
|
217
|
-
LastUpdateTime = if type(lockData.LastUpdateTime) == "number" then lockData.LastUpdateTime else nil,
|
|
218
|
-
ActiveSession = activeSession,
|
|
219
|
-
}
|
|
220
|
-
end
|
|
221
|
-
|
|
222
144
|
function DataStoreLockHelper.PromiseCloseSession(self: DataStoreLockHelper): Promise.Promise<()>
|
|
223
145
|
return self._sessionClosedPromise
|
|
224
146
|
end
|
|
@@ -245,7 +167,7 @@ function DataStoreLockHelper.AcquireLock(self: DataStoreLockHelper, data: any, c
|
|
|
245
167
|
}
|
|
246
168
|
end
|
|
247
169
|
|
|
248
|
-
local parsedLockData =
|
|
170
|
+
local parsedLockData = DataStoreLockUtils.deserializeLockData(data.lock)
|
|
249
171
|
if parsedLockData == nil or parsedLockData.ActiveSession == nil then
|
|
250
172
|
return {
|
|
251
173
|
isValid = true,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
Entry point for the datastore package. Registers every server-side datastore service, so a
|
|
4
|
+
consumer gets the package in one [ServiceBag.GetService].
|
|
5
|
+
|
|
6
|
+
@server
|
|
7
|
+
@class DataStoreService
|
|
8
|
+
]=]
|
|
9
|
+
|
|
10
|
+
local require = require(script.Parent.loader).load(script)
|
|
11
|
+
|
|
12
|
+
local ServiceBag = require("ServiceBag")
|
|
13
|
+
|
|
14
|
+
local DataStoreService = {}
|
|
15
|
+
DataStoreService.ServiceName = "DataStoreService"
|
|
16
|
+
|
|
17
|
+
export type DataStoreService = typeof(setmetatable(
|
|
18
|
+
{} :: {
|
|
19
|
+
_serviceBag: ServiceBag.ServiceBag,
|
|
20
|
+
},
|
|
21
|
+
{} :: typeof({ __index = DataStoreService })
|
|
22
|
+
))
|
|
23
|
+
|
|
24
|
+
function DataStoreService.Init(self: DataStoreService, serviceBag: ServiceBag.ServiceBag)
|
|
25
|
+
assert(not (self :: any)._serviceBag, "Already initialized")
|
|
26
|
+
self._serviceBag = assert(serviceBag, "No serviceBag")
|
|
27
|
+
|
|
28
|
+
-- External
|
|
29
|
+
self._serviceBag:GetService(require("BindToCloseService"))
|
|
30
|
+
self._serviceBag:GetService(require("PlaceMessagingService"))
|
|
31
|
+
self._serviceBag:GetService(require("CmdrService"))
|
|
32
|
+
|
|
33
|
+
-- Internal
|
|
34
|
+
self._serviceBag:GetService(require("DataStoreCmdrService"))
|
|
35
|
+
self._serviceBag:GetService(require("GameDataStoreService"))
|
|
36
|
+
self._serviceBag:GetService(require("PlayerDataStoreService"))
|
|
37
|
+
self._serviceBag:GetService(require("PrivateServerDataStoreService"))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
return DataStoreService
|