@quenty/datastore 8.0.0-canary.367.e9fdcbc.0 → 8.0.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 CHANGED
@@ -3,7 +3,142 @@
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
- # [8.0.0-canary.367.e9fdcbc.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/datastore@7.15.0...@quenty/datastore@8.0.0-canary.367.e9fdcbc.0) (2023-06-05)
6
+ # [8.0.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/datastore@7.26.1...@quenty/datastore@8.0.0) (2023-10-11)
7
+
8
+ **Note:** Version bump only for package @quenty/datastore
9
+
10
+
11
+
12
+
13
+
14
+ ## [7.26.1](https://github.com/Quenty/NevermoreEngine/compare/@quenty/datastore@7.26.0...@quenty/datastore@7.26.1) (2023-09-21)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * Fix datastore writer copying non-table scenario ([3303b36](https://github.com/Quenty/NevermoreEngine/commit/3303b36511ca9a3d89ffa711dfc5723276166d55))
20
+
21
+
22
+
23
+
24
+
25
+ # [7.26.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/datastore@7.25.2...@quenty/datastore@7.26.0) (2023-09-21)
26
+
27
+ **Note:** Version bump only for package @quenty/datastore
28
+
29
+
30
+
31
+
32
+
33
+ ## [7.25.2](https://github.com/Quenty/NevermoreEngine/compare/@quenty/datastore@7.25.1...@quenty/datastore@7.25.2) (2023-09-19)
34
+
35
+
36
+ ### Bug Fixes
37
+
38
+ * Ensure default value is returned in certain nil scenarios ([f9032d0](https://github.com/Quenty/NevermoreEngine/commit/f9032d0d465231f86f55a60c6465183731567b4c))
39
+
40
+
41
+
42
+
43
+
44
+ ## [7.25.1](https://github.com/Quenty/NevermoreEngine/compare/@quenty/datastore@7.25.0...@quenty/datastore@7.25.1) (2023-09-07)
45
+
46
+
47
+ ### Bug Fixes
48
+
49
+ * Fix actual code for data store match ([ed39424](https://github.com/Quenty/NevermoreEngine/commit/ed3942460c7abbd3f21f988e562d0e5b84aeec05))
50
+ * Fix issue where DataStoreStringUtils.isValidUTF8(str) may not detect invalid strings ([88682b0](https://github.com/Quenty/NevermoreEngine/commit/88682b0600be6642fd44dc9db09a124eca46433c))
51
+
52
+
53
+
54
+
55
+
56
+ # [7.25.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/datastore@7.24.0...@quenty/datastore@7.25.0) (2023-09-04)
57
+
58
+ **Note:** Version bump only for package @quenty/datastore
59
+
60
+
61
+
62
+
63
+
64
+ # [7.24.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/datastore@7.23.0...@quenty/datastore@7.24.0) (2023-08-23)
65
+
66
+
67
+ ### Bug Fixes
68
+
69
+ * Changed events weren't firing for the datastore stage ([04f2cf9](https://github.com/Quenty/NevermoreEngine/commit/04f2cf921fcc5e5c7db8ed16b8c76a0bc06c5688))
70
+ * Fix additional components but data store writing ping-pings back and forth ([667015d](https://github.com/Quenty/NevermoreEngine/commit/667015d65fe44856076346394c4b218bead012b5))
71
+ * More data store improvements ([b4c5918](https://github.com/Quenty/NevermoreEngine/commit/b4c5918055ffde9e3c5041e98432a13f4e8c913a))
72
+
73
+
74
+ ### Features
75
+
76
+ * DataStores appear to be working, but require more testing ([ef03495](https://github.com/Quenty/NevermoreEngine/commit/ef0349554ea29f1812e39bb4bc833adc09c92968))
77
+ * More untested datastore syncing code ([276909d](https://github.com/Quenty/NevermoreEngine/commit/276909dadc819e07b78727d56c95b597760fcf6e))
78
+ * Semi-broken datastore changes ([7e9a32b](https://github.com/Quenty/NevermoreEngine/commit/7e9a32bbadebd729305f36bba965d2c47b14d6d9))
79
+ * Unfinished datastore changes ([d627fa0](https://github.com/Quenty/NevermoreEngine/commit/d627fa0a36f2a733deead8b63b1b72be4e1c3a9f))
80
+
81
+
82
+
83
+
84
+
85
+ # [7.23.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/datastore@7.22.0...@quenty/datastore@7.23.0) (2023-08-01)
86
+
87
+ **Note:** Version bump only for package @quenty/datastore
88
+
89
+
90
+
91
+
92
+
93
+ # [7.22.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/datastore@7.21.0...@quenty/datastore@7.22.0) (2023-07-28)
94
+
95
+ **Note:** Version bump only for package @quenty/datastore
96
+
97
+
98
+
99
+
100
+
101
+ # [7.21.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/datastore@7.20.0...@quenty/datastore@7.21.0) (2023-07-23)
102
+
103
+ **Note:** Version bump only for package @quenty/datastore
104
+
105
+
106
+
107
+
108
+
109
+ # [7.20.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/datastore@7.19.0...@quenty/datastore@7.20.0) (2023-07-15)
110
+
111
+ **Note:** Version bump only for package @quenty/datastore
112
+
113
+
114
+
115
+
116
+
117
+ # [7.19.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/datastore@7.18.0...@quenty/datastore@7.19.0) (2023-07-12)
118
+
119
+ **Note:** Version bump only for package @quenty/datastore
120
+
121
+
122
+
123
+
124
+
125
+ # [7.18.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/datastore@7.17.0...@quenty/datastore@7.18.0) (2023-07-10)
126
+
127
+ **Note:** Version bump only for package @quenty/datastore
128
+
129
+
130
+
131
+
132
+
133
+ # [7.17.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/datastore@7.16.0...@quenty/datastore@7.17.0) (2023-06-17)
134
+
135
+ **Note:** Version bump only for package @quenty/datastore
136
+
137
+
138
+
139
+
140
+
141
+ # [7.16.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/datastore@7.15.0...@quenty/datastore@7.16.0) (2023-06-05)
7
142
 
8
143
  **Note:** Version bump only for package @quenty/datastore
9
144
 
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": "8.0.0-canary.367.e9fdcbc.0",
3
+ "version": "8.0.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": "6.2.1",
30
- "@quenty/bindtocloseservice": "3.0.0-canary.367.e9fdcbc.0",
31
- "@quenty/loader": "6.2.1",
32
- "@quenty/maid": "2.5.0",
33
- "@quenty/promise": "6.5.0",
34
- "@quenty/rx": "7.11.0",
35
- "@quenty/signal": "2.4.0",
36
- "@quenty/symbol": "2.2.0",
37
- "@quenty/table": "3.2.0"
29
+ "@quenty/baseobject": "^7.0.0",
30
+ "@quenty/bindtocloseservice": "^3.0.0",
31
+ "@quenty/loader": "^7.0.0",
32
+ "@quenty/maid": "^2.6.0",
33
+ "@quenty/math": "^2.5.0",
34
+ "@quenty/promise": "^7.0.0",
35
+ "@quenty/rx": "^8.0.0",
36
+ "@quenty/servicebag": "^7.0.0",
37
+ "@quenty/signal": "^3.0.0",
38
+ "@quenty/symbol": "^2.2.0",
39
+ "@quenty/table": "^3.3.0",
40
+ "@quenty/valueobject": "^8.0.0"
38
41
  },
39
42
  "publishConfig": {
40
43
  "access": "public"
41
44
  },
42
- "gitHead": "e9fdcbc6ea1d46e068bf42a08b833099e9005259"
45
+ "gitHead": "fdeae46099587019ec5fc15317dc673aed379400"
43
46
  }
@@ -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)
@@ -40,7 +40,7 @@
40
40
  playerMoneyValue.Value = 0
41
41
  playerMoneyValue.Parent = player
42
42
 
43
- maid:GivePromise(playerDataStoreService:PromiseDataStore(Players)):Then(function(dataStore)
43
+ maid:GivePromise(playerDataStoreService:PromiseDataStore(player)):Then(function(dataStore)
44
44
  maid:GivePromise(dataStore:Load("money", 0))
45
45
  :Then(function(money)
46
46
  playerMoneyValue.Value = money
@@ -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 DEBUG_WRITING = false
78
+ local DEFAULT_DEBUG_WRITING = false
76
79
 
77
- local AUTO_SAVE_TIME = 60*5
78
- local CHECK_DIVISION = 15
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,28 @@ 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._cacheTimeSeconds = DEFAULT_CACHE_TIME_SECONDS
103
+ self._debugWriting = DEFAULT_DEBUG_WRITING
104
+
105
+ self._autoSaveTimeSeconds = self._maid:Add(ValueObject.new(DEFAULT_AUTO_SAVE_TIME_SECONDS))
106
+ self._jitterProportion = self._maid:Add(ValueObject.new(DEFAULT_JITTER_PROPORTION, "number"))
107
+ self._syncOnSave = self._maid:Add(ValueObject.new(false, "boolean"))
108
+ self._loadedOk = self._maid:Add(ValueObject.new(false, "boolean"))
109
+
110
+ self._userIdList = nil
98
111
 
99
112
  if self._key == "" then
100
113
  error("[DataStore] - Key cannot be an empty string")
@@ -108,41 +121,20 @@ function DataStore.new(robloxDataStore, key)
108
121
  self.Saving = Signal.new() -- :Fire(promise)
109
122
  self._maid:GiveTask(self.Saving)
110
123
 
111
- task.spawn(function()
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)
124
+ self:_setupAutoSaving()
134
125
 
135
126
  return self
136
127
  end
137
128
 
138
129
  --[=[
139
- Sets how long the datastore will cache for
140
- @param cacheTimeSeconds number?
130
+ Set to true to debug writing this data store
131
+
132
+ @param debugWriting boolean
141
133
  ]=]
142
- function DataStore:SetCacheTime(cacheTimeSeconds)
143
- assert(type(cacheTimeSeconds) == "number" or cacheTimeSeconds == nil, "Bad cacheTimeSeconds")
134
+ function DataStore:SetDoDebugWriting(debugWriting)
135
+ assert(type(debugWriting) == "boolean", "Bad debugWriting")
144
136
 
145
- self._cacheTimeSeconds = cacheTimeSeconds or DEFAULT_CACHE_TIME_SECONDS
137
+ self._debugWriting = debugWriting
146
138
  end
147
139
 
148
140
  --[=[
@@ -153,16 +145,39 @@ function DataStore:GetFullPath()
153
145
  return ("RobloxDataStore@%s"):format(self._key)
154
146
  end
155
147
 
148
+ --[=[
149
+ How frequent the data store will autosave (or sync) to the cloud. If set to nil then the datastore
150
+ will not do any syncing.
151
+
152
+ @param autoSaveTimeSeconds number | nil
153
+ ]=]
154
+ function DataStore:SetAutoSaveTimeSeconds(autoSaveTimeSeconds)
155
+ assert(type(autoSaveTimeSeconds) == "number" or autoSaveTimeSeconds == nil, "Bad autoSaveTimeSeconds")
156
+
157
+ self._autoSaveTimeSeconds.Value = autoSaveTimeSeconds
158
+ end
159
+
160
+ --[=[
161
+ How frequent the data store will autosave (or sync) to the cloud
162
+
163
+ @param syncEnabled boolean
164
+ ]=]
165
+ function DataStore:SetSyncOnSave(syncEnabled)
166
+ assert(type(syncEnabled) == "boolean", "Bad syncEnabled")
167
+
168
+ self._syncOnSave.Value = syncEnabled
169
+ end
170
+
156
171
  --[=[
157
172
  Returns whether the datastore failed.
158
173
  @return boolean
159
174
  ]=]
160
175
  function DataStore:DidLoadFail()
161
- if not self._loadPromise then
176
+ if not self._firstLoadPromise then
162
177
  return false
163
178
  end
164
179
 
165
- if self._loadPromise:IsRejected() then
180
+ if self._firstLoadPromise:IsRejected() then
166
181
  return true
167
182
  end
168
183
 
@@ -175,7 +190,7 @@ end
175
190
  @return Promise<boolean>
176
191
  ]=]
177
192
  function DataStore:PromiseLoadSuccessful()
178
- return self._maid:GivePromise(self:_promiseLoad()):Then(function()
193
+ return self._maid:GivePromise(self:PromiseViewUpToDate()):Then(function()
179
194
  return true
180
195
  end, function()
181
196
  return false
@@ -187,67 +202,208 @@ end
187
202
  @return Promise
188
203
  ]=]
189
204
  function DataStore:Save()
190
- if self:DidLoadFail() then
191
- warn("[DataStore] - Not saving, failed to load")
192
- return Promise.rejected("Load not successful, not saving")
205
+ return self:_syncData(false)
206
+ end
207
+
208
+ --[=[
209
+ Same as saving the data but it also loads fresh data from the datastore, which may consume
210
+ additional data-store query calls.
211
+
212
+ @return Promise
213
+ ]=]
214
+ function DataStore:Sync()
215
+ return self:_syncData(true)
216
+ end
217
+
218
+ --[=[
219
+ Sets the user id list associated with this datastore. Can be useful for GDPR compliance.
220
+
221
+ @param userIdList { number } | nil
222
+ ]=]
223
+ function DataStore:SetUserIdList(userIdList)
224
+ assert(type(userIdList) == "table" or userIdList == nil, "Bad userIdList")
225
+
226
+ self._userIdList = userIdList
227
+ end
228
+
229
+ --[=[
230
+ Returns a list of user ids or nil
231
+
232
+ @return { number } | nil
233
+ ]=]
234
+ function DataStore:GetUserIdList()
235
+ return self._userIdList
236
+ end
237
+
238
+ --[=[
239
+ Overridden helper method for data store stage below.
240
+
241
+ @return Promise
242
+ ]=]
243
+ function DataStore:PromiseViewUpToDate()
244
+ if self._firstLoadPromise then
245
+ return self._firstLoadPromise
193
246
  end
194
247
 
195
- if DEBUG_WRITING then
196
- print("[DataStore.Save] - Starting save routine")
248
+ self._firstLoadPromise = self:_promiseGetAsyncNoCache()
249
+
250
+ self._firstLoadPromise:Tap(function()
251
+ self._loadedOk.Value = true
252
+ end)
253
+
254
+ return self._firstLoadPromise
255
+ end
256
+
257
+ function DataStore:_setupAutoSaving()
258
+ local startTime = os.clock()
259
+
260
+ self._maid:GiveTask(Rx.combineLatest({
261
+ autoSaveTimeSeconds = self._autoSaveTimeSeconds:Observe();
262
+ jitterProportion = self._jitterProportion:Observe();
263
+ syncOnSave = self._syncOnSave:Observe();
264
+ loadedOk = self._loadedOk:Observe();
265
+ }):Subscribe(function(state)
266
+ if state.autoSaveTimeSeconds and state.loadedOk then
267
+ local maid = Maid.new()
268
+ if self._debugWriting then
269
+ print("Auto-saving loop started")
270
+ end
271
+
272
+ -- TODO: First jitter is way noisier to differentiate servers
273
+ maid:GiveTask(task.spawn(function()
274
+ while true do
275
+ local jitterBase = math.random()
276
+ local timeElapsed = os.clock() - startTime
277
+ local totalWaitTime = Math.jitter(state.autoSaveTimeSeconds, state.jitterProportion*state.autoSaveTimeSeconds, jitterBase)
278
+ local timeRemaining = totalWaitTime - timeElapsed
279
+
280
+ if timeRemaining > 0 then
281
+ task.wait(timeRemaining)
282
+ end
283
+
284
+ startTime = os.clock()
285
+
286
+ if state.syncOnSave then
287
+ self:Sync()
288
+ else
289
+ self:Save()
290
+ end
291
+
292
+ task.wait(0.1)
293
+ end
294
+ end))
295
+
296
+ self._maid._autoSavingMaid = maid
297
+ else
298
+ self._maid._autoSavingMaid = nil
299
+ end
300
+ end))
301
+ end
302
+
303
+ function DataStore:_syncData(doMergeNewData)
304
+ if self:DidLoadFail() then
305
+ warn("[DataStore] - Not syncing, failed to load")
306
+ return Promise.rejected("Load not successful, not syncing")
197
307
  end
198
308
 
199
- -- Avoid constructing promises for every callback down the datastore
200
- -- upon save.
201
- return (self:_promiseInvokeSavingCallbacks() or Promise.resolved())
309
+ return self._maid:GivePromise(self:PromiseViewUpToDate())
310
+ :Then(function()
311
+ return self._maid:GivePromise(self:PromiseInvokeSavingCallbacks())
312
+ end)
202
313
  :Then(function()
203
314
  if not self:HasWritableData() then
315
+ if doMergeNewData then
316
+ -- Reads are cheaper than update async calls
317
+ return self:_promiseGetAsyncNoCache()
318
+ end
319
+
204
320
  -- Nothing to save, don't update anything
205
- if DEBUG_WRITING then
206
- print("[DataStore.Save] - Not saving, nothing staged")
321
+ if self._debugWriting then
322
+ print("[DataStore] - Not saving, nothing staged")
207
323
  end
324
+
208
325
  return nil
209
326
  else
210
- return self:_saveData(self:GetNewWriter())
327
+ return self:_doDataSync(self:GetNewWriter(), doMergeNewData)
211
328
  end
212
329
  end)
213
330
  end
214
331
 
215
- --[=[
216
- Loads data. This returns the originally loaded data.
217
- @param keyName string
218
- @param defaultValue any?
219
- @return any?
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
332
+ function DataStore:_doDataSync(writer, doMergeNewData)
333
+ assert(type(doMergeNewData) == "boolean", "Bad doMergeNewData")
334
+
335
+ -- Cache user id list
336
+ writer:SetUserIdList(self:GetUserIdList())
227
337
 
228
- function DataStore:_saveData(writer)
229
338
  local maid = Maid.new()
230
339
 
231
340
  local promise = Promise.new()
232
- promise:Resolve(maid:GivePromise(DataStorePromises.updateAsync(self._robloxDataStore, self._key, function(data)
233
- if promise:IsRejected() then
234
- -- Cancel if we have another request
235
- return nil
341
+
342
+ if writer:IsCompleteWipe() then
343
+ if self._debugWriting then
344
+ print(string.format("[DataStore] - DataStorePromises.removeAsync(%q)", self._key))
236
345
  end
237
346
 
238
- data = writer:WriteMerge(data or {})
239
- assert(data ~= DataStoreDeleteToken, "Cannot delete from UpdateAsync")
347
+ -- This is, of course, dangerous, because we won't merge
348
+ promise:Resolve(maid:GivePromise(DataStorePromises.removeAsync(self._robloxDataStore, self._key)):Then(function()
349
+ if doMergeNewData then
350
+ -- Write our data
351
+ self:MarkDataAsSaved(writer)
240
352
 
241
- if DEBUG_WRITING then
242
- print("[DataStore] - Writing", game:GetService("HttpService"):JSONEncode(data))
353
+ -- Do syncing after
354
+ return self:_promiseGetAsyncNoCache()
355
+ end
356
+ end))
357
+ else
358
+ if self._debugWriting then
359
+ print(string.format("[DataStore] - DataStorePromises.updateAsync(%q) with doMergeNewData = %s", self._key, tostring(doMergeNewData)))
243
360
  end
244
361
 
245
- return data
246
- end, function(err)
362
+ promise:Resolve(maid:GivePromise(DataStorePromises.updateAsync(self._robloxDataStore, self._key, function(original, datastoreKeyInfo)
363
+ if promise:IsRejected() then
364
+ -- Cancel if we have another request
365
+ return nil
366
+ end
367
+
368
+ local diffSnapshot
369
+ if doMergeNewData then
370
+ diffSnapshot = writer:ComputeDiffSnapshot(original)
371
+ end
372
+
373
+ local result = writer:WriteMerge(original)
374
+
375
+ if result == DataStoreDeleteToken or result == nil then
376
+ result = {}
377
+ end
378
+
379
+ if self._debugWriting then
380
+ print("[DataStore] - Writing", result)
381
+ end
382
+
383
+ if doMergeNewData then
384
+ -- This prevents resaving at high frequency
385
+ self:MarkDataAsSaved(writer)
386
+ self:MergeDiffSnapshot(diffSnapshot)
387
+ end
388
+
389
+ local userIdList = writer:GetUserIdList()
390
+ if datastoreKeyInfo then
391
+ userIdList = datastoreKeyInfo:GetUserIds()
392
+ end
393
+
394
+ local metadata = nil
395
+ if datastoreKeyInfo then
396
+ metadata = datastoreKeyInfo:GetMetadata()
397
+ end
398
+
399
+ return result, userIdList, metadata
400
+ end)))
401
+ end
402
+
403
+ promise:Tap(nil, function(err)
247
404
  -- Might be caused by Maid rejecting state
248
- warn("[DataStore] - Failed to UpdateAsync data", err)
249
- return Promise.rejected(err)
250
- end)))
405
+ warn("[DataStore] - Failed to sync data", err)
406
+ end)
251
407
 
252
408
  self._maid._saveMaid = maid
253
409
 
@@ -258,27 +414,23 @@ function DataStore:_saveData(writer)
258
414
  return promise
259
415
  end
260
416
 
261
- function DataStore:_promiseLoad()
262
- if self._loadPromise then
263
- return self._loadPromise
264
- end
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)
417
+ function DataStore:_promiseGetAsyncNoCache()
418
+ return self._maid:GivePromise(DataStorePromises.getAsync(self._robloxDataStore, self._key))
419
+ :Catch(function(err)
420
+ warn(string.format("DataStorePromises.getAsync(%q) -> warning - ", self._key), err)
278
421
  return Promise.rejected(err)
279
- end))
422
+ end)
423
+ :Then(function(data)
424
+ local writer = self:GetNewWriter()
425
+ local diffSnapshot = writer:ComputeDiffSnapshot(data)
280
426
 
281
- return self._loadPromise
427
+ self:MergeDiffSnapshot(diffSnapshot)
428
+
429
+ if self._debugWriting then
430
+ print(string.format("DataStorePromises.getAsync(%q) -> Got ", self._key), data, "with diff snapshot", diffSnapshot, "to view", self._viewSnapshot)
431
+ -- print(string.format("DataStorePromises.getAsync(%q) -> Got ", self._key), data)
432
+ end
433
+ end)
282
434
  end
283
435
 
284
436
  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 dataStore:Save()
41
+ return Promise.defer(function(resolve)
42
+ return resolve(dataStore:Save())
43
+ end)
38
44
  end))
39
45
 
40
46
  return dataStore