@quenty/datastore 7.23.0 → 7.23.1-canary.402.40f9a1f.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/CHANGELOG.md +20 -0
- package/README.md +11 -0
- package/package.json +14 -11
- package/src/Server/DataStore.lua +254 -95
- package/src/Server/GameDataStoreService.lua +7 -1
- package/src/Server/Modules/DataStoreSnapshotUtils.lua +13 -0
- package/src/Server/Modules/DataStoreStage.lua +724 -242
- package/src/Server/Modules/DataStoreWriter.lua +235 -25
- package/src/Server/PlayerDataStoreManager.lua +1 -0
- package/src/Server/Utility/DataStorePromises.lua +3 -2
- package/test/default.project.json +21 -0
- package/test/scripts/Client/ClientMain.client.lua +10 -0
- package/test/scripts/Server/ServerMain.server.lua +120 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,26 @@
|
|
|
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
|
+
## [7.23.1-canary.402.40f9a1f.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/datastore@7.23.0...@quenty/datastore@7.23.1-canary.402.40f9a1f.0) (2023-08-16)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* Fix additional components but data store writing ping-pings back and forth ([667015d](https://github.com/Quenty/NevermoreEngine/commit/667015d65fe44856076346394c4b218bead012b5))
|
|
12
|
+
* More data store improvements ([b4c5918](https://github.com/Quenty/NevermoreEngine/commit/b4c5918055ffde9e3c5041e98432a13f4e8c913a))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* DataStores appear to be working, but require more testing ([ef03495](https://github.com/Quenty/NevermoreEngine/commit/ef0349554ea29f1812e39bb4bc833adc09c92968))
|
|
18
|
+
* More untested datastore syncing code ([276909d](https://github.com/Quenty/NevermoreEngine/commit/276909dadc819e07b78727d56c95b597760fcf6e))
|
|
19
|
+
* Semi-broken datastore changes ([7e9a32b](https://github.com/Quenty/NevermoreEngine/commit/7e9a32bbadebd729305f36bba965d2c47b14d6d9))
|
|
20
|
+
* Unfinished datastore changes ([d627fa0](https://github.com/Quenty/NevermoreEngine/commit/d627fa0a36f2a733deead8b63b1b72be4e1c3a9f))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
6
26
|
# [7.23.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/datastore@7.22.0...@quenty/datastore@7.23.0) (2023-08-01)
|
|
7
27
|
|
|
8
28
|
**Note:** Version bump only for package @quenty/datastore
|
package/README.md
CHANGED
|
@@ -15,6 +15,17 @@ This system is a reliable datastore system designed with promises and asyncronio
|
|
|
15
15
|
|
|
16
16
|
<div align="center"><a href="https://quenty.github.io/NevermoreEngine/api/DataStore">View docs →</a></div>
|
|
17
17
|
|
|
18
|
+
## Executive overiew
|
|
19
|
+
This datastore prevents data loss by being explicit about what we're writing to, and only modifying the data that exists there instead of modifying the whole structure.
|
|
20
|
+
|
|
21
|
+
## How syncing works
|
|
22
|
+
Sometimes datastores (like a global game data store) need to be synced live instead of upon server or player start. This is if we expect multiple servers to write to the same datastore at once we can use thie sync method to
|
|
23
|
+
|
|
24
|
+
Syncing is like saving. However, instead of treating the current datastore as a session lock, we load in additional data from our "source-of-truth". From here, we merge that data into the datastore, which means both clearing any matching write tokens that our sync says is done.
|
|
25
|
+
|
|
26
|
+
This is best for a "shared" memory that can be temporarily not correct. Deleting with a sync is less effective.
|
|
27
|
+
|
|
28
|
+
|
|
18
29
|
## Installation
|
|
19
30
|
```
|
|
20
31
|
npm install @quenty/datastore --save
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/datastore",
|
|
3
|
-
"version": "7.23.0",
|
|
3
|
+
"version": "7.23.1-canary.402.40f9a1f.0",
|
|
4
4
|
"description": "Quenty's Datastore implementation for Roblox",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Roblox",
|
|
@@ -26,18 +26,21 @@
|
|
|
26
26
|
"Quenty"
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@quenty/baseobject": "
|
|
30
|
-
"@quenty/bindtocloseservice": "
|
|
31
|
-
"@quenty/loader": "
|
|
32
|
-
"@quenty/maid": "
|
|
33
|
-
"@quenty/
|
|
34
|
-
"@quenty/
|
|
35
|
-
"@quenty/
|
|
36
|
-
"@quenty/
|
|
37
|
-
"@quenty/
|
|
29
|
+
"@quenty/baseobject": "6.2.2-canary.402.40f9a1f.0",
|
|
30
|
+
"@quenty/bindtocloseservice": "2.18.1-canary.402.40f9a1f.0",
|
|
31
|
+
"@quenty/loader": "6.2.2-canary.402.40f9a1f.0",
|
|
32
|
+
"@quenty/maid": "2.5.0",
|
|
33
|
+
"@quenty/math": "2.4.1-canary.402.40f9a1f.0",
|
|
34
|
+
"@quenty/promise": "6.7.1-canary.402.40f9a1f.0",
|
|
35
|
+
"@quenty/rx": "7.14.1-canary.402.40f9a1f.0",
|
|
36
|
+
"@quenty/servicebag": "6.8.1-canary.402.40f9a1f.0",
|
|
37
|
+
"@quenty/signal": "2.4.0",
|
|
38
|
+
"@quenty/symbol": "2.2.0",
|
|
39
|
+
"@quenty/table": "3.2.1-canary.402.40f9a1f.0",
|
|
40
|
+
"@quenty/valueobject": "7.21.1-canary.402.40f9a1f.0"
|
|
38
41
|
},
|
|
39
42
|
"publishConfig": {
|
|
40
43
|
"access": "public"
|
|
41
44
|
},
|
|
42
|
-
"gitHead": "
|
|
45
|
+
"gitHead": "40f9a1fad543e137f1e639cafc45a98cb439b0b6"
|
|
43
46
|
}
|
package/src/Server/DataStore.lua
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
local playerMoneyValue = Instance.new("IntValue")
|
|
12
12
|
playerMoneyValue.Value = 0
|
|
13
13
|
|
|
14
|
-
local dataStore = DataStore.new(DataStoreService:GetDataStore("test"), test-store")
|
|
14
|
+
local dataStore = DataStore.new(DataStoreService:GetDataStore("test"), "test-store")
|
|
15
15
|
dataStore:Load("money", 0):Then(function(money)
|
|
16
16
|
playerMoneyValue.Value = money
|
|
17
17
|
dataStore:StoreOnValueChange("money", playerMoneyValue)
|
|
@@ -71,13 +71,14 @@ local DataStoreStage = require("DataStoreStage")
|
|
|
71
71
|
local Maid = require("Maid")
|
|
72
72
|
local Promise = require("Promise")
|
|
73
73
|
local Signal = require("Signal")
|
|
74
|
+
local Math = require("Math")
|
|
75
|
+
local ValueObject = require("ValueObject")
|
|
76
|
+
local Rx = require("Rx")
|
|
74
77
|
|
|
75
|
-
local
|
|
78
|
+
local DEFAULT_DEBUG_WRITING = false
|
|
76
79
|
|
|
77
|
-
local
|
|
78
|
-
local
|
|
79
|
-
local JITTER = 20 -- Randomly assign jitter so if a ton of players join at once we don't hit the datastore at once
|
|
80
|
-
local DEFAULT_CACHE_TIME_SECONDS = math.huge
|
|
80
|
+
local DEFAULT_AUTO_SAVE_TIME_SECONDS = 60*5
|
|
81
|
+
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
|
|
81
82
|
|
|
82
83
|
local DataStore = setmetatable({}, DataStoreStage)
|
|
83
84
|
DataStore.ClassName = "DataStore"
|
|
@@ -85,16 +86,35 @@ DataStore.__index = DataStore
|
|
|
85
86
|
|
|
86
87
|
--[=[
|
|
87
88
|
Constructs a new DataStore. See [DataStoreStage] for more API.
|
|
89
|
+
|
|
90
|
+
```lua
|
|
91
|
+
local dataStore = serviceBag:GetService(PlayerDataStoreService):PromiseDataStore(player):Yield()
|
|
92
|
+
```
|
|
93
|
+
|
|
88
94
|
@param robloxDataStore DataStore
|
|
89
95
|
@param key string
|
|
90
96
|
@return DataStore
|
|
91
97
|
]=]
|
|
92
98
|
function DataStore.new(robloxDataStore, key)
|
|
93
|
-
local self = setmetatable(DataStoreStage.new(), DataStore)
|
|
99
|
+
local self = setmetatable(DataStoreStage.new(key), DataStore)
|
|
94
100
|
|
|
95
101
|
self._key = key or error("No key")
|
|
96
102
|
self._robloxDataStore = robloxDataStore or error("No robloxDataStore")
|
|
97
|
-
self.
|
|
103
|
+
self._debugWriting = DEFAULT_DEBUG_WRITING
|
|
104
|
+
|
|
105
|
+
self._autoSaveTimeSeconds = ValueObject.new(DEFAULT_AUTO_SAVE_TIME_SECONDS)
|
|
106
|
+
self._maid:GiveTask(self._autoSaveTimeSeconds)
|
|
107
|
+
|
|
108
|
+
self._jitterProportion = ValueObject.new(DEFAULT_JITTER_PROPORTION, "number")
|
|
109
|
+
self._maid:GiveTask(self._jitterProportion)
|
|
110
|
+
|
|
111
|
+
self._syncOnSave = ValueObject.new(false, "boolean")
|
|
112
|
+
self._maid:GiveTask(self._syncOnSave)
|
|
113
|
+
|
|
114
|
+
self._loadedOk = ValueObject.new(false, "boolean")
|
|
115
|
+
self._maid:GiveTask(self._loadedOk)
|
|
116
|
+
|
|
117
|
+
self._userIdList = nil
|
|
98
118
|
|
|
99
119
|
if self._key == "" then
|
|
100
120
|
error("[DataStore] - Key cannot be an empty string")
|
|
@@ -108,41 +128,20 @@ function DataStore.new(robloxDataStore, key)
|
|
|
108
128
|
self.Saving = Signal.new() -- :Fire(promise)
|
|
109
129
|
self._maid:GiveTask(self.Saving)
|
|
110
130
|
|
|
111
|
-
|
|
112
|
-
while self.Destroy do
|
|
113
|
-
for _=1, CHECK_DIVISION do
|
|
114
|
-
task.wait(AUTO_SAVE_TIME/CHECK_DIVISION)
|
|
115
|
-
if not self.Destroy then
|
|
116
|
-
break
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
if not self.Destroy then
|
|
121
|
-
break
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
-- Apply additional jitter on auto-save
|
|
125
|
-
task.wait(math.random(1, JITTER))
|
|
126
|
-
|
|
127
|
-
if not self.Destroy then
|
|
128
|
-
break
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
self:Save()
|
|
132
|
-
end
|
|
133
|
-
end)
|
|
131
|
+
self:_setupAutoSaving()
|
|
134
132
|
|
|
135
133
|
return self
|
|
136
134
|
end
|
|
137
135
|
|
|
138
136
|
--[=[
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
Set to true to debug writing this data store
|
|
138
|
+
|
|
139
|
+
@param debugWriting boolean
|
|
141
140
|
]=]
|
|
142
|
-
function DataStore:
|
|
143
|
-
assert(type(
|
|
141
|
+
function DataStore:SetDoDebugWriting(debugWriting)
|
|
142
|
+
assert(type(debugWriting) == "boolean", "Bad debugWriting")
|
|
144
143
|
|
|
145
|
-
self.
|
|
144
|
+
self._debugWriting = debugWriting
|
|
146
145
|
end
|
|
147
146
|
|
|
148
147
|
--[=[
|
|
@@ -153,16 +152,39 @@ function DataStore:GetFullPath()
|
|
|
153
152
|
return ("RobloxDataStore@%s"):format(self._key)
|
|
154
153
|
end
|
|
155
154
|
|
|
155
|
+
--[=[
|
|
156
|
+
How frequent the data store will autosave (or sync) to the cloud. If set to nil then the datastore
|
|
157
|
+
will not do any syncing.
|
|
158
|
+
|
|
159
|
+
@param autoSaveTimeSeconds number | nil
|
|
160
|
+
]=]
|
|
161
|
+
function DataStore:SetAutoSaveTimeSeconds(autoSaveTimeSeconds)
|
|
162
|
+
assert(type(autoSaveTimeSeconds) == "number" or autoSaveTimeSeconds == nil, "Bad autoSaveTimeSeconds")
|
|
163
|
+
|
|
164
|
+
self._autoSaveTimeSeconds.Value = autoSaveTimeSeconds
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
--[=[
|
|
168
|
+
How frequent the data store will autosave (or sync) to the cloud
|
|
169
|
+
|
|
170
|
+
@param syncEnabled boolean
|
|
171
|
+
]=]
|
|
172
|
+
function DataStore:SetSyncOnSave(syncEnabled)
|
|
173
|
+
assert(type(syncEnabled) == "boolean", "Bad syncEnabled")
|
|
174
|
+
|
|
175
|
+
self._syncOnSave.Value = syncEnabled
|
|
176
|
+
end
|
|
177
|
+
|
|
156
178
|
--[=[
|
|
157
179
|
Returns whether the datastore failed.
|
|
158
180
|
@return boolean
|
|
159
181
|
]=]
|
|
160
182
|
function DataStore:DidLoadFail()
|
|
161
|
-
if not self.
|
|
183
|
+
if not self._firstLoadPromise then
|
|
162
184
|
return false
|
|
163
185
|
end
|
|
164
186
|
|
|
165
|
-
if self.
|
|
187
|
+
if self._firstLoadPromise:IsRejected() then
|
|
166
188
|
return true
|
|
167
189
|
end
|
|
168
190
|
|
|
@@ -175,7 +197,7 @@ end
|
|
|
175
197
|
@return Promise<boolean>
|
|
176
198
|
]=]
|
|
177
199
|
function DataStore:PromiseLoadSuccessful()
|
|
178
|
-
return self._maid:GivePromise(self:
|
|
200
|
+
return self._maid:GivePromise(self:PromiseViewUpToDate()):Then(function()
|
|
179
201
|
return true
|
|
180
202
|
end, function()
|
|
181
203
|
return false
|
|
@@ -187,67 +209,208 @@ end
|
|
|
187
209
|
@return Promise
|
|
188
210
|
]=]
|
|
189
211
|
function DataStore:Save()
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
212
|
+
return self:_syncData(false)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
--[=[
|
|
216
|
+
Same as saving the data but it also loads fresh data from the datastore, which may consume
|
|
217
|
+
additional data-store query calls.
|
|
218
|
+
|
|
219
|
+
@return Promise
|
|
220
|
+
]=]
|
|
221
|
+
function DataStore:Sync()
|
|
222
|
+
return self:_syncData(true)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
--[=[
|
|
226
|
+
Sets the user id list associated with this datastore. Can be useful for GDPR compliance.
|
|
227
|
+
|
|
228
|
+
@param userIdList { number } | nil
|
|
229
|
+
]=]
|
|
230
|
+
function DataStore:SetUserIdList(userIdList)
|
|
231
|
+
assert(type(userIdList) == "table" or userIdList == nil, "Bad userIdList")
|
|
232
|
+
|
|
233
|
+
self._userIdList = userIdList
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
--[=[
|
|
237
|
+
Returns a list of user ids or nil
|
|
238
|
+
|
|
239
|
+
@return { number } | nil
|
|
240
|
+
]=]
|
|
241
|
+
function DataStore:GetUserIdList()
|
|
242
|
+
return self._userIdList
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
--[=[
|
|
246
|
+
Overridden helper method for data store stage below.
|
|
247
|
+
|
|
248
|
+
@return Promise
|
|
249
|
+
]=]
|
|
250
|
+
function DataStore:PromiseViewUpToDate()
|
|
251
|
+
if self._firstLoadPromise then
|
|
252
|
+
return self._firstLoadPromise
|
|
193
253
|
end
|
|
194
254
|
|
|
195
|
-
|
|
196
|
-
|
|
255
|
+
self._firstLoadPromise = self:_promiseGetAsyncNoCache()
|
|
256
|
+
|
|
257
|
+
self._firstLoadPromise:Tap(function()
|
|
258
|
+
self._loadedOk.Value = true
|
|
259
|
+
end)
|
|
260
|
+
|
|
261
|
+
return self._firstLoadPromise
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
function DataStore:_setupAutoSaving()
|
|
265
|
+
local startTime = os.clock()
|
|
266
|
+
|
|
267
|
+
self._maid:GiveTask(Rx.combineLatest({
|
|
268
|
+
autoSaveTimeSeconds = self._autoSaveTimeSeconds:Observe();
|
|
269
|
+
jitterProportion = self._jitterProportion:Observe();
|
|
270
|
+
syncOnSave = self._syncOnSave:Observe();
|
|
271
|
+
loadedOk = self._loadedOk:Observe();
|
|
272
|
+
}):Subscribe(function(state)
|
|
273
|
+
if state.autoSaveTimeSeconds and state.loadedOk then
|
|
274
|
+
local maid = Maid.new()
|
|
275
|
+
if self._debugWriting then
|
|
276
|
+
print("Auto-saving loop started")
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
-- TODO: First jitter is way noisier to differentiate servers
|
|
280
|
+
maid:GiveTask(task.spawn(function()
|
|
281
|
+
while true do
|
|
282
|
+
local jitterBase = math.random()
|
|
283
|
+
local timeElapsed = os.clock() - startTime
|
|
284
|
+
local totalWaitTime = Math.jitter(state.autoSaveTimeSeconds, state.jitterProportion*state.autoSaveTimeSeconds, jitterBase)
|
|
285
|
+
local timeRemaining = totalWaitTime - timeElapsed
|
|
286
|
+
|
|
287
|
+
if timeRemaining > 0 then
|
|
288
|
+
task.wait(timeRemaining)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
startTime = os.clock()
|
|
292
|
+
|
|
293
|
+
if state.syncOnSave then
|
|
294
|
+
self:Sync()
|
|
295
|
+
else
|
|
296
|
+
self:Save()
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
task.wait(0.1)
|
|
300
|
+
end
|
|
301
|
+
end))
|
|
302
|
+
|
|
303
|
+
self._maid._autoSavingMaid = maid
|
|
304
|
+
else
|
|
305
|
+
self._maid._autoSavingMaid = nil
|
|
306
|
+
end
|
|
307
|
+
end))
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
function DataStore:_syncData(doMergeNewData)
|
|
311
|
+
if self:DidLoadFail() then
|
|
312
|
+
warn("[DataStore] - Not syncing, failed to load")
|
|
313
|
+
return Promise.rejected("Load not successful, not syncing")
|
|
197
314
|
end
|
|
198
315
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
316
|
+
return self._maid:GivePromise(self:PromiseViewUpToDate())
|
|
317
|
+
:Then(function()
|
|
318
|
+
return self._maid:GivePromise(self:PromiseInvokeSavingCallbacks())
|
|
319
|
+
end)
|
|
202
320
|
:Then(function()
|
|
203
321
|
if not self:HasWritableData() then
|
|
322
|
+
if doMergeNewData then
|
|
323
|
+
-- Reads are cheaper than update async calls
|
|
324
|
+
return self:_promiseGetAsyncNoCache()
|
|
325
|
+
end
|
|
326
|
+
|
|
204
327
|
-- Nothing to save, don't update anything
|
|
205
|
-
if
|
|
206
|
-
print("[DataStore
|
|
328
|
+
if self._debugWriting then
|
|
329
|
+
print("[DataStore] - Not saving, nothing staged")
|
|
207
330
|
end
|
|
331
|
+
|
|
208
332
|
return nil
|
|
209
333
|
else
|
|
210
|
-
return self:
|
|
334
|
+
return self:_doDataSync(self:GetNewWriter(), doMergeNewData)
|
|
211
335
|
end
|
|
212
336
|
end)
|
|
213
337
|
end
|
|
214
338
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
]=]
|
|
221
|
-
function DataStore:Load(keyName, defaultValue)
|
|
222
|
-
return self:_promiseLoad()
|
|
223
|
-
:Then(function(data)
|
|
224
|
-
return self:_afterLoadGetAndApplyStagedData(keyName, data, defaultValue)
|
|
225
|
-
end)
|
|
226
|
-
end
|
|
339
|
+
function DataStore:_doDataSync(writer, doMergeNewData)
|
|
340
|
+
assert(type(doMergeNewData) == "boolean", "Bad doMergeNewData")
|
|
341
|
+
|
|
342
|
+
-- Cache user id list
|
|
343
|
+
writer:SetUserIdList(self:GetUserIdList())
|
|
227
344
|
|
|
228
|
-
function DataStore:_saveData(writer)
|
|
229
345
|
local maid = Maid.new()
|
|
230
346
|
|
|
231
347
|
local promise = Promise.new()
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
348
|
+
|
|
349
|
+
if writer:IsCompleteWipe() then
|
|
350
|
+
if self._debugWriting then
|
|
351
|
+
print(string.format("[DataStore] - DataStorePromises.removeAsync(%q)", self._key))
|
|
236
352
|
end
|
|
237
353
|
|
|
238
|
-
|
|
239
|
-
|
|
354
|
+
-- This is, of course, dangerous, because we won't merge
|
|
355
|
+
promise:Resolve(maid:GivePromise(DataStorePromises.removeAsync(self._robloxDataStore, self._key)):Then(function()
|
|
356
|
+
if doMergeNewData then
|
|
357
|
+
-- Write our data
|
|
358
|
+
self:MarkDataAsSaved(writer)
|
|
240
359
|
|
|
241
|
-
|
|
242
|
-
|
|
360
|
+
-- Do syncing after
|
|
361
|
+
return self:_promiseGetAsyncNoCache()
|
|
362
|
+
end
|
|
363
|
+
end))
|
|
364
|
+
else
|
|
365
|
+
if self._debugWriting then
|
|
366
|
+
print(string.format("[DataStore] - DataStorePromises.updateAsync(%q)", self._key))
|
|
243
367
|
end
|
|
244
368
|
|
|
245
|
-
|
|
246
|
-
|
|
369
|
+
promise:Resolve(maid:GivePromise(DataStorePromises.updateAsync(self._robloxDataStore, self._key, function(original, datastoreKeyInfo)
|
|
370
|
+
if promise:IsRejected() then
|
|
371
|
+
-- Cancel if we have another request
|
|
372
|
+
return nil
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
local diffSnapshot
|
|
376
|
+
if doMergeNewData then
|
|
377
|
+
diffSnapshot = writer:ComputeDiffSnapshot(original)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
local result = writer:WriteMerge(original)
|
|
381
|
+
|
|
382
|
+
if result == DataStoreDeleteToken or result == nil then
|
|
383
|
+
result = {}
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
if self._debugWriting then
|
|
387
|
+
print("[DataStore] - Writing", result)
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
if doMergeNewData then
|
|
391
|
+
-- This prevents resaving at high frequency
|
|
392
|
+
self:MarkDataAsSaved(writer)
|
|
393
|
+
self:MergeDiffSnapshot(diffSnapshot)
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
local userIdList = writer:GetUserIdList()
|
|
397
|
+
if datastoreKeyInfo then
|
|
398
|
+
userIdList = datastoreKeyInfo:GetUserIds()
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
local metadata = nil
|
|
402
|
+
if datastoreKeyInfo then
|
|
403
|
+
metadata = datastoreKeyInfo:GetMetadata()
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
return result, userIdList, metadata
|
|
407
|
+
end)))
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
promise:Tap(nil, function(err)
|
|
247
411
|
-- Might be caused by Maid rejecting state
|
|
248
|
-
warn("[DataStore] - Failed to
|
|
249
|
-
|
|
250
|
-
end)))
|
|
412
|
+
warn("[DataStore] - Failed to sync data", err)
|
|
413
|
+
end)
|
|
251
414
|
|
|
252
415
|
self._maid._saveMaid = maid
|
|
253
416
|
|
|
@@ -258,27 +421,23 @@ function DataStore:_saveData(writer)
|
|
|
258
421
|
return promise
|
|
259
422
|
end
|
|
260
423
|
|
|
261
|
-
function DataStore:
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
self._loadPromise = self._maid:GivePromise(DataStorePromises.getAsync(self._robloxDataStore, self._key)
|
|
267
|
-
:Then(function(data)
|
|
268
|
-
if data == nil then
|
|
269
|
-
return {}
|
|
270
|
-
elseif type(data) == "table" then
|
|
271
|
-
return data
|
|
272
|
-
else
|
|
273
|
-
return Promise.rejected("Failed to load data. Wrong type '" .. type(data) .. "'")
|
|
274
|
-
end
|
|
275
|
-
end, function(err)
|
|
276
|
-
-- Log:
|
|
277
|
-
warn("[DataStore] - Failed to GetAsync data", err)
|
|
424
|
+
function DataStore:_promiseGetAsyncNoCache()
|
|
425
|
+
return self._maid:GivePromise(DataStorePromises.getAsync(self._robloxDataStore, self._key))
|
|
426
|
+
:Catch(function(err)
|
|
427
|
+
warn(string.format("DataStorePromises.getAsync(%q) -> warning - ", self._key), err)
|
|
278
428
|
return Promise.rejected(err)
|
|
279
|
-
end)
|
|
429
|
+
end)
|
|
430
|
+
:Then(function(data)
|
|
431
|
+
local writer = self:GetNewWriter()
|
|
432
|
+
local diffSnapshot = writer:ComputeDiffSnapshot(data)
|
|
280
433
|
|
|
281
|
-
|
|
434
|
+
self:MergeDiffSnapshot(diffSnapshot)
|
|
435
|
+
|
|
436
|
+
if self._debugWriting then
|
|
437
|
+
print(string.format("DataStorePromises.getAsync(%q) -> Got ", self._key), data, "with diff snapshot", diffSnapshot, "to view", self._viewSnapshot)
|
|
438
|
+
-- print(string.format("DataStorePromises.getAsync(%q) -> Got ", self._key), data)
|
|
439
|
+
end
|
|
440
|
+
end)
|
|
282
441
|
end
|
|
283
442
|
|
|
284
443
|
return DataStore
|
|
@@ -10,6 +10,7 @@ local require = require(script.Parent.loader).load(script)
|
|
|
10
10
|
local DataStore = require("DataStore")
|
|
11
11
|
local DataStorePromises = require("DataStorePromises")
|
|
12
12
|
local Maid = require("Maid")
|
|
13
|
+
local Promise = require("Promise")
|
|
13
14
|
|
|
14
15
|
local GameDataStoreService = {}
|
|
15
16
|
GameDataStoreService.ServiceName = "GameDataStoreService"
|
|
@@ -30,11 +31,16 @@ function GameDataStoreService:PromiseDataStore()
|
|
|
30
31
|
|
|
31
32
|
self._dataStorePromise = self:_promiseRobloxDataStore()
|
|
32
33
|
:Then(function(robloxDataStore)
|
|
34
|
+
-- Live sync this stuff pretty frequently
|
|
33
35
|
local dataStore = DataStore.new(robloxDataStore, self:_getKey())
|
|
36
|
+
dataStore:SetSyncOnSave(true)
|
|
37
|
+
dataStore:SetAutoSaveTimeSeconds(15)
|
|
34
38
|
self._maid:GiveTask(dataStore)
|
|
35
39
|
|
|
36
40
|
self._maid:GiveTask(self._bindToCloseService:RegisterPromiseOnCloseCallback(function()
|
|
37
|
-
return
|
|
41
|
+
return Promise.defer(function(resolve)
|
|
42
|
+
return resolve(dataStore:Save())
|
|
43
|
+
end)
|
|
38
44
|
end))
|
|
39
45
|
|
|
40
46
|
return dataStore
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
--[=[
|
|
2
|
+
@class DataStoreSnapshotUtils
|
|
3
|
+
]=]
|
|
4
|
+
|
|
5
|
+
local require = require(script.Parent.loader).load(script)
|
|
6
|
+
|
|
7
|
+
local DataStoreSnapshotUtils = {}
|
|
8
|
+
|
|
9
|
+
function DataStoreSnapshotUtils.isEmptySnapshot(snapshot)
|
|
10
|
+
return type(snapshot) == "table" and next(snapshot) == nil
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
return DataStoreSnapshotUtils
|