@quenty/datastore 13.22.1-canary.39d0eda.0 → 13.22.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 +2 -5
- package/package.json +15 -16
- package/src/Server/DataStore.lua +30 -182
- package/src/Server/PlayerDataStoreManager.lua +2 -7
package/CHANGELOG.md
CHANGED
|
@@ -3,12 +3,9 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
-
## [13.22.1
|
|
6
|
+
## [13.22.1](https://github.com/Quenty/NevermoreEngine/compare/@quenty/datastore@13.22.0...@quenty/datastore@13.22.1) (2025-08-12)
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
### Features
|
|
10
|
-
|
|
11
|
-
* Add datastore session locking system ([cf6e9b7](https://github.com/Quenty/NevermoreEngine/commit/cf6e9b712f45efa34b8957fbad96aaf289b28b3e))
|
|
8
|
+
**Note:** Version bump only for package @quenty/datastore
|
|
12
9
|
|
|
13
10
|
|
|
14
11
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/datastore",
|
|
3
|
-
"version": "13.22.1
|
|
3
|
+
"version": "13.22.1",
|
|
4
4
|
"description": "Quenty's Datastore implementation for Roblox",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Roblox",
|
|
@@ -26,23 +26,22 @@
|
|
|
26
26
|
"Quenty"
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@quenty/baseobject": "10.9.0",
|
|
30
|
-
"@quenty/bindtocloseservice": "8.18.1
|
|
31
|
-
"@quenty/loader": "10.9.0",
|
|
32
|
-
"@quenty/maid": "3.5.0",
|
|
33
|
-
"@quenty/math": "2.7.3",
|
|
34
|
-
"@quenty/pagesutils": "5.12.
|
|
35
|
-
"@quenty/promise": "10.11.
|
|
36
|
-
"@quenty/
|
|
37
|
-
"@quenty/
|
|
38
|
-
"@quenty/
|
|
39
|
-
"@quenty/
|
|
40
|
-
"@quenty/
|
|
41
|
-
"@quenty/
|
|
42
|
-
"@quenty/valueobject": "13.18.1-canary.39d0eda.0"
|
|
29
|
+
"@quenty/baseobject": "^10.9.0",
|
|
30
|
+
"@quenty/bindtocloseservice": "^8.18.1",
|
|
31
|
+
"@quenty/loader": "^10.9.0",
|
|
32
|
+
"@quenty/maid": "^3.5.0",
|
|
33
|
+
"@quenty/math": "^2.7.3",
|
|
34
|
+
"@quenty/pagesutils": "^5.12.0",
|
|
35
|
+
"@quenty/promise": "^10.11.0",
|
|
36
|
+
"@quenty/rx": "^13.18.1",
|
|
37
|
+
"@quenty/servicebag": "^11.13.1",
|
|
38
|
+
"@quenty/signal": "^7.11.1",
|
|
39
|
+
"@quenty/symbol": "^3.5.0",
|
|
40
|
+
"@quenty/table": "^3.8.0",
|
|
41
|
+
"@quenty/valueobject": "^13.18.1"
|
|
43
42
|
},
|
|
44
43
|
"publishConfig": {
|
|
45
44
|
"access": "public"
|
|
46
45
|
},
|
|
47
|
-
"gitHead": "
|
|
46
|
+
"gitHead": "847e9e65e518392c1e2f30669ee2589ed3d7e870"
|
|
48
47
|
}
|
package/src/Server/DataStore.lua
CHANGED
|
@@ -66,16 +66,12 @@
|
|
|
66
66
|
|
|
67
67
|
local require = require(script.Parent.loader).load(script)
|
|
68
68
|
|
|
69
|
-
local RunService = game:GetService("RunService")
|
|
70
|
-
|
|
71
69
|
local DataStoreDeleteToken = require("DataStoreDeleteToken")
|
|
72
70
|
local DataStorePromises = require("DataStorePromises")
|
|
73
71
|
local DataStoreStage = require("DataStoreStage")
|
|
74
72
|
local Maid = require("Maid")
|
|
75
73
|
local Math = require("Math")
|
|
76
74
|
local Promise = require("Promise")
|
|
77
|
-
local PromiseMaidUtils = require("PromiseMaidUtils")
|
|
78
|
-
local PromiseRetryUtils = require("PromiseRetryUtils")
|
|
79
75
|
local Rx = require("Rx")
|
|
80
76
|
local Signal = require("Signal")
|
|
81
77
|
local Symbol = require("Symbol")
|
|
@@ -85,7 +81,6 @@ local DEFAULT_DEBUG_WRITING = false
|
|
|
85
81
|
|
|
86
82
|
local DEFAULT_AUTO_SAVE_TIME_SECONDS = 60 * 5
|
|
87
83
|
local DEFAULT_JITTER_PROPORTION = 0.1 -- Randomly assign jitter so if a ton of players join at once we don't hit the datastore at once
|
|
88
|
-
local UNLOCK_BY_DEFAULT_TIME_MULTIPLIER = 2.1
|
|
89
84
|
|
|
90
85
|
local DataStore = setmetatable({}, DataStoreStage)
|
|
91
86
|
DataStore.ClassName = "DataStore"
|
|
@@ -97,13 +92,11 @@ export type DataStore = typeof(setmetatable(
|
|
|
97
92
|
_userIdList: { number }?,
|
|
98
93
|
_robloxDataStore: DataStorePromises.RobloxDataStore,
|
|
99
94
|
_debugWriting: boolean,
|
|
100
|
-
_sessionLockingEnabled: boolean,
|
|
101
95
|
_autoSaveTimeSeconds: ValueObject.ValueObject<number?>,
|
|
102
96
|
_jitterProportion: ValueObject.ValueObject<number>,
|
|
103
97
|
_syncOnSave: ValueObject.ValueObject<boolean>,
|
|
104
98
|
_loadedOk: ValueObject.ValueObject<boolean>,
|
|
105
99
|
_firstLoadPromise: Promise.Promise<()>,
|
|
106
|
-
_promiseSessionLockingFailed: Promise.Promise<()>,
|
|
107
100
|
Saving: Signal.Signal<Promise.Promise<()>>,
|
|
108
101
|
},
|
|
109
102
|
{} :: typeof({ __index = DataStore })
|
|
@@ -131,7 +124,6 @@ function DataStore.new(robloxDataStore: DataStorePromises.RobloxDataStore, key:
|
|
|
131
124
|
self._jitterProportion = self._maid:Add(ValueObject.new(DEFAULT_JITTER_PROPORTION, "number"))
|
|
132
125
|
self._syncOnSave = self._maid:Add(ValueObject.new(false, "boolean"))
|
|
133
126
|
self._loadedOk = self._maid:Add(ValueObject.new(false, "boolean"))
|
|
134
|
-
self._promiseSessionLockingFailed = self._maid:Add(Promise.new())
|
|
135
127
|
|
|
136
128
|
self._userIdList = nil
|
|
137
129
|
|
|
@@ -151,27 +143,6 @@ function DataStore.new(robloxDataStore: DataStorePromises.RobloxDataStore, key:
|
|
|
151
143
|
return self
|
|
152
144
|
end
|
|
153
145
|
|
|
154
|
-
--[=[
|
|
155
|
-
Sets session locking enabled
|
|
156
|
-
|
|
157
|
-
@param sessionLockingEnabled boolean
|
|
158
|
-
]=]
|
|
159
|
-
function DataStore.SetSessionLockingEnabled(self: DataStore, sessionLockingEnabled: boolean)
|
|
160
|
-
assert(not self._firstLoadPromise, "Must set session locking before datastore is loaded")
|
|
161
|
-
|
|
162
|
-
self._sessionLockingEnabled = sessionLockingEnabled
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
--[=[
|
|
166
|
-
Returns a promise that rejects on datastore cleanup, and resolves only when the session locking
|
|
167
|
-
code completely fails
|
|
168
|
-
|
|
169
|
-
@return Promise<>
|
|
170
|
-
]=]
|
|
171
|
-
function DataStore.PromiseSessionLockingFailed(self: DataStore)
|
|
172
|
-
return self._promiseSessionLockingFailed
|
|
173
|
-
end
|
|
174
|
-
|
|
175
146
|
--[=[
|
|
176
147
|
Set to true to debug writing this data store
|
|
177
148
|
|
|
@@ -251,16 +222,6 @@ function DataStore.Save(self: DataStore): Promise.Promise<()>
|
|
|
251
222
|
return self:_syncData(false)
|
|
252
223
|
end
|
|
253
224
|
|
|
254
|
-
--[=[
|
|
255
|
-
Saves all stored data.
|
|
256
|
-
@return Promise
|
|
257
|
-
]=]
|
|
258
|
-
function DataStore.SaveAndCloseSession(self: DataStore): Promise.Promise<()>
|
|
259
|
-
assert(self._sessionLockingEnabled, "Cannot invoke unless session locking is enabled")
|
|
260
|
-
|
|
261
|
-
return self:_syncData(false, true)
|
|
262
|
-
end
|
|
263
|
-
|
|
264
225
|
--[=[
|
|
265
226
|
Same as saving the data but it also loads fresh data from the datastore, which may consume
|
|
266
227
|
additional data-store query calls.
|
|
@@ -362,7 +323,7 @@ function DataStore._setupAutoSaving(self: DataStore)
|
|
|
362
323
|
end))
|
|
363
324
|
end
|
|
364
325
|
|
|
365
|
-
function DataStore._syncData(self: DataStore, doMergeNewData: boolean
|
|
326
|
+
function DataStore._syncData(self: DataStore, doMergeNewData: boolean)
|
|
366
327
|
if self:DidLoadFail() then
|
|
367
328
|
warn("[DataStore] - Not syncing, failed to load")
|
|
368
329
|
return Promise.rejected("Load not successful, not syncing")
|
|
@@ -374,7 +335,7 @@ function DataStore._syncData(self: DataStore, doMergeNewData: boolean, doCloseSe
|
|
|
374
335
|
return self._maid:GivePromise(self:PromiseInvokeSavingCallbacks())
|
|
375
336
|
end)
|
|
376
337
|
:Then(function(): Promise.Promise<()>?
|
|
377
|
-
if not self:HasWritableData()
|
|
338
|
+
if not self:HasWritableData() then
|
|
378
339
|
if doMergeNewData then
|
|
379
340
|
-- Reads are cheaper than update async calls
|
|
380
341
|
return self:_promiseGetAsyncNoCache()
|
|
@@ -387,17 +348,12 @@ function DataStore._syncData(self: DataStore, doMergeNewData: boolean, doCloseSe
|
|
|
387
348
|
|
|
388
349
|
return nil
|
|
389
350
|
else
|
|
390
|
-
return self:_doDataSync(self:GetNewWriter(), doMergeNewData
|
|
351
|
+
return self:_doDataSync(self:GetNewWriter(), doMergeNewData)
|
|
391
352
|
end
|
|
392
353
|
end)
|
|
393
354
|
end
|
|
394
355
|
|
|
395
|
-
function DataStore._doDataSync(
|
|
396
|
-
self: DataStore,
|
|
397
|
-
writer,
|
|
398
|
-
doMergeNewData: boolean,
|
|
399
|
-
doCloseSession: boolean?
|
|
400
|
-
): Promise.Promise<()>
|
|
356
|
+
function DataStore._doDataSync(self: DataStore, writer, doMergeNewData: boolean): Promise.Promise<()>
|
|
401
357
|
assert(type(doMergeNewData) == "boolean", "Bad doMergeNewData")
|
|
402
358
|
|
|
403
359
|
-- Cache user id list
|
|
@@ -440,11 +396,6 @@ function DataStore._doDataSync(
|
|
|
440
396
|
promise:Resolve(
|
|
441
397
|
maid:GivePromise(
|
|
442
398
|
DataStorePromises.updateAsync(self._robloxDataStore, self._key, function(original, datastoreKeyInfo)
|
|
443
|
-
if self._sessionLockingEnabled then
|
|
444
|
-
original = table.clone(original)
|
|
445
|
-
original.lock = nil
|
|
446
|
-
end
|
|
447
|
-
|
|
448
399
|
if promise:IsRejected() then
|
|
449
400
|
-- Cancel if we have another request
|
|
450
401
|
return nil
|
|
@@ -483,17 +434,6 @@ function DataStore._doDataSync(
|
|
|
483
434
|
metadata = datastoreKeyInfo:GetMetadata()
|
|
484
435
|
end
|
|
485
436
|
|
|
486
|
-
if doCloseSession then
|
|
487
|
-
result = table.clone(result)
|
|
488
|
-
result.lock = nil
|
|
489
|
-
elseif self._sessionLockingEnabled then
|
|
490
|
-
-- Maintain the lock with the latest time
|
|
491
|
-
result = table.clone(result)
|
|
492
|
-
if result.lock then
|
|
493
|
-
result.lock = os.time()
|
|
494
|
-
end
|
|
495
|
-
end
|
|
496
|
-
|
|
497
437
|
return result, userIdList, metadata
|
|
498
438
|
end)
|
|
499
439
|
)
|
|
@@ -515,128 +455,36 @@ function DataStore._doDataSync(
|
|
|
515
455
|
end
|
|
516
456
|
|
|
517
457
|
function DataStore._promiseGetAsyncNoCache(self: DataStore): Promise.Promise<()>
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
DataStorePromises.updateAsync(self._robloxDataStore, self._key, function(data, datastoreKeyInfo)
|
|
527
|
-
local userIdList = self._userIdList
|
|
528
|
-
if datastoreKeyInfo then
|
|
529
|
-
userIdList = datastoreKeyInfo:GetUserIds()
|
|
530
|
-
end
|
|
531
|
-
|
|
532
|
-
local metadata = nil
|
|
533
|
-
if datastoreKeyInfo then
|
|
534
|
-
metadata = datastoreKeyInfo:GetMetadata()
|
|
535
|
-
end
|
|
536
|
-
|
|
537
|
-
if self._debugWriting then
|
|
538
|
-
print(string.format("DataStorePromises.updateAsync(%q) -> Got ", tostring(self._key)), data)
|
|
539
|
-
end
|
|
540
|
-
|
|
541
|
-
if data.lock then
|
|
542
|
-
local isInvalidLock = false
|
|
543
|
-
if type(data.lock) == "number" then
|
|
544
|
-
local timeElapsed = os.time() - data.lock
|
|
545
|
-
local autoSaveSeconds = self._autoSaveTimeSeconds.Value
|
|
546
|
-
if
|
|
547
|
-
autoSaveSeconds
|
|
548
|
-
and timeElapsed > (autoSaveSeconds * UNLOCK_BY_DEFAULT_TIME_MULTIPLIER)
|
|
549
|
-
then
|
|
550
|
-
isInvalidLock = true
|
|
551
|
-
end
|
|
552
|
-
end
|
|
553
|
-
|
|
554
|
-
-- Allow data locked to load in studio because otherwise testing gets really messy
|
|
555
|
-
if RunService:IsStudio() then
|
|
556
|
-
isInvalidLock = true
|
|
557
|
-
end
|
|
558
|
-
|
|
559
|
-
if not isInvalidLock then
|
|
560
|
-
loadPromise:Reject(string.format("Profile is locked (%s)", tostring(data.lock)))
|
|
561
|
-
|
|
562
|
-
-- Cancel write to avoid maintaining lock
|
|
563
|
-
return nil
|
|
564
|
-
end
|
|
565
|
-
|
|
566
|
-
-- Be sure to cleanup stuff
|
|
567
|
-
data.lock = nil :: number?
|
|
568
|
-
end
|
|
569
|
-
|
|
570
|
-
-- TODO: Retry
|
|
571
|
-
loadPromise:Resolve(data)
|
|
572
|
-
|
|
573
|
-
data.lock = os.time()
|
|
574
|
-
|
|
575
|
-
return data, userIdList, metadata
|
|
576
|
-
end)
|
|
458
|
+
return self._maid
|
|
459
|
+
:GivePromise(DataStorePromises.getAsync(self._robloxDataStore, self._key))
|
|
460
|
+
:Catch(function(err)
|
|
461
|
+
warn(
|
|
462
|
+
string.format(
|
|
463
|
+
"DataStorePromises.getAsync(%q) -> warning - %s",
|
|
464
|
+
tostring(self._key),
|
|
465
|
+
tostring(err or "empty error")
|
|
577
466
|
)
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
467
|
+
)
|
|
468
|
+
return Promise.rejected(err)
|
|
469
|
+
end)
|
|
470
|
+
:Then(function(data)
|
|
471
|
+
local writer = self:GetNewWriter()
|
|
472
|
+
local diffSnapshot = writer:ComputeDiffSnapshot(data)
|
|
583
473
|
|
|
584
|
-
|
|
585
|
-
end
|
|
474
|
+
self:MergeDiffSnapshot(diffSnapshot)
|
|
586
475
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
}))
|
|
596
|
-
:Catch(function(err)
|
|
597
|
-
warn(
|
|
598
|
-
string.format(
|
|
599
|
-
"DataStorePromises.updateAsync(%q) -> warning - %s",
|
|
600
|
-
tostring(self._key),
|
|
601
|
-
tostring(err or "empty error")
|
|
602
|
-
)
|
|
603
|
-
)
|
|
604
|
-
self._promiseSessionLockingFailed:Resolve()
|
|
605
|
-
return Promise.rejected(err)
|
|
606
|
-
end)
|
|
607
|
-
else
|
|
608
|
-
promise = self._maid
|
|
609
|
-
:GivePromise(DataStorePromises.getAsync(self._robloxDataStore, self._key))
|
|
610
|
-
:Catch(function(err)
|
|
611
|
-
warn(
|
|
612
|
-
string.format(
|
|
613
|
-
"DataStorePromises.getAsync(%q) -> warning - %s",
|
|
614
|
-
tostring(self._key),
|
|
615
|
-
tostring(err or "empty error")
|
|
616
|
-
)
|
|
476
|
+
if self._debugWriting then
|
|
477
|
+
print(
|
|
478
|
+
string.format("DataStorePromises.getAsync(%q) -> Got ", tostring(self._key)),
|
|
479
|
+
data,
|
|
480
|
+
"with diff snapshot",
|
|
481
|
+
diffSnapshot,
|
|
482
|
+
"to view",
|
|
483
|
+
self._viewSnapshot
|
|
617
484
|
)
|
|
618
|
-
|
|
619
|
-
end
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
return promise:Then(function(data)
|
|
623
|
-
local writer = self:GetNewWriter()
|
|
624
|
-
local diffSnapshot = writer:ComputeDiffSnapshot(data)
|
|
625
|
-
|
|
626
|
-
self:MergeDiffSnapshot(diffSnapshot)
|
|
627
|
-
|
|
628
|
-
if self._debugWriting then
|
|
629
|
-
print(
|
|
630
|
-
string.format("DataStorePromises.getAsync(%q) -> Got ", tostring(self._key)),
|
|
631
|
-
data,
|
|
632
|
-
"with diff snapshot",
|
|
633
|
-
diffSnapshot,
|
|
634
|
-
"to view",
|
|
635
|
-
self._viewSnapshot
|
|
636
|
-
)
|
|
637
|
-
-- print(string.format("DataStorePromises.getAsync(%q) -> Got ", self._key), data)
|
|
638
|
-
end
|
|
639
|
-
end)
|
|
485
|
+
-- print(string.format("DataStorePromises.getAsync(%q) -> Got ", self._key), data)
|
|
486
|
+
end
|
|
487
|
+
end)
|
|
640
488
|
end
|
|
641
489
|
|
|
642
490
|
return DataStore
|
|
@@ -131,7 +131,7 @@ end
|
|
|
131
131
|
For if you want to disable saving in studio for faster close time!
|
|
132
132
|
]=]
|
|
133
133
|
function PlayerDataStoreManager.DisableSaveOnCloseStudio(self: PlayerDataStoreManager): ()
|
|
134
|
-
assert(RunService:IsStudio()
|
|
134
|
+
assert(RunService:IsStudio())
|
|
135
135
|
|
|
136
136
|
self._disableSavingInStudio = true
|
|
137
137
|
end
|
|
@@ -196,13 +196,8 @@ function PlayerDataStoreManager._createDataStore(self: PlayerDataStoreManager, p
|
|
|
196
196
|
assert(not self._datastores[player], "Bad player")
|
|
197
197
|
|
|
198
198
|
local datastore = DataStore.new(self._robloxDataStore, self:_getKey(player))
|
|
199
|
-
datastore:SetSessionLockingEnabled(true)
|
|
200
199
|
datastore:SetUserIdList({ player.UserId })
|
|
201
200
|
|
|
202
|
-
datastore:PromiseSessionLockingFailed():Then(function()
|
|
203
|
-
player:Kick("DataStore session lock failed to load. Please message developers.")
|
|
204
|
-
end)
|
|
205
|
-
|
|
206
201
|
self._maid._savingConns[player] = datastore.Saving:Connect(function(promise)
|
|
207
202
|
self._pendingSaves:Add(promise)
|
|
208
203
|
end)
|
|
@@ -233,7 +228,7 @@ function PlayerDataStoreManager._removePlayerDataStore(self: PlayerDataStoreMana
|
|
|
233
228
|
|
|
234
229
|
PromiseUtils.all(removingPromises)
|
|
235
230
|
:Then(function()
|
|
236
|
-
return datastore:
|
|
231
|
+
return datastore:Save()
|
|
237
232
|
end)
|
|
238
233
|
:Finally(function()
|
|
239
234
|
datastore:Destroy()
|