@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 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-canary.39d0eda.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/datastore@13.22.0...@quenty/datastore@13.22.1-canary.39d0eda.0) (2025-08-29)
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-canary.39d0eda.0",
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-canary.39d0eda.0",
31
- "@quenty/loader": "10.9.0",
32
- "@quenty/maid": "3.5.0",
33
- "@quenty/math": "2.7.3",
34
- "@quenty/pagesutils": "5.12.1-canary.39d0eda.0",
35
- "@quenty/promise": "10.11.1-canary.39d0eda.0",
36
- "@quenty/promisemaid": "5.11.1-canary.39d0eda.0",
37
- "@quenty/rx": "13.18.1-canary.39d0eda.0",
38
- "@quenty/servicebag": "11.13.1-canary.39d0eda.0",
39
- "@quenty/signal": "7.11.1-canary.39d0eda.0",
40
- "@quenty/symbol": "3.5.0",
41
- "@quenty/table": "3.8.0",
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": "39d0edaa45d0c4812b43f2a64ad543fac3f79423"
46
+ "gitHead": "847e9e65e518392c1e2f30669ee2589ed3d7e870"
48
47
  }
@@ -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, doCloseSession: 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() and not doCloseSession then
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, doCloseSession)
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
- local promise
519
- if self._sessionLockingEnabled then
520
- local function promiseLoadUnlockedProfile()
521
- local loadPromise = Promise.new()
522
- self._maid[loadPromise] = loadPromise
523
-
524
- PromiseMaidUtils.whilePromise(loadPromise, function(maid)
525
- maid:GivePromise(
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
- end)
579
-
580
- loadPromise:Finally(function()
581
- self._maid[loadPromise] = nil
582
- end)
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
- return loadPromise
585
- end
474
+ self:MergeDiffSnapshot(diffSnapshot)
586
475
 
587
- promise = self._maid
588
- :Add(PromiseRetryUtils.retry(promiseLoadUnlockedProfile, {
589
- -- https://exponentialbackoffcalculator.com/
590
- -- ~10 minutes
591
- exponential = 1.5,
592
- initialWaitTime = 1,
593
- maxAttempts = 15,
594
- printWarning = true,
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
- return Promise.rejected(err)
619
- end)
620
- end
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(), "Must invoke in studio")
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:SaveAndCloseSession()
231
+ return datastore:Save()
237
232
  end)
238
233
  :Finally(function()
239
234
  datastore:Destroy()