@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 +136 -1
- package/README.md +11 -0
- package/package.json +14 -11
- package/src/Server/DataStore.lua +248 -96
- package/src/Server/GameDataStoreService.lua +7 -1
- package/src/Server/Modules/DataStoreSnapshotUtils.lua +13 -0
- package/src/Server/Modules/DataStoreStage.lua +758 -241
- package/src/Server/Modules/DataStoreWriter.lua +235 -25
- package/src/Server/PlayerDataStoreManager.lua +1 -0
- package/src/Server/Utility/DataStorePromises.lua +3 -2
- package/src/Shared/Utility/DataStoreStringUtils.lua +7 -3
- 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,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
|
|
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
|
|
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": "
|
|
30
|
-
"@quenty/bindtocloseservice": "3.0.0
|
|
31
|
-
"@quenty/loader": "
|
|
32
|
-
"@quenty/maid": "2.
|
|
33
|
-
"@quenty/
|
|
34
|
-
"@quenty/
|
|
35
|
-
"@quenty/
|
|
36
|
-
"@quenty/
|
|
37
|
-
"@quenty/
|
|
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": "
|
|
45
|
+
"gitHead": "fdeae46099587019ec5fc15317dc673aed379400"
|
|
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)
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
playerMoneyValue.Value = 0
|
|
41
41
|
playerMoneyValue.Parent = player
|
|
42
42
|
|
|
43
|
-
maid:GivePromise(playerDataStoreService:PromiseDataStore(
|
|
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
|
|
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,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.
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
130
|
+
Set to true to debug writing this data store
|
|
131
|
+
|
|
132
|
+
@param debugWriting boolean
|
|
141
133
|
]=]
|
|
142
|
-
function DataStore:
|
|
143
|
-
assert(type(
|
|
134
|
+
function DataStore:SetDoDebugWriting(debugWriting)
|
|
135
|
+
assert(type(debugWriting) == "boolean", "Bad debugWriting")
|
|
144
136
|
|
|
145
|
-
self.
|
|
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.
|
|
176
|
+
if not self._firstLoadPromise then
|
|
162
177
|
return false
|
|
163
178
|
end
|
|
164
179
|
|
|
165
|
-
if self.
|
|
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:
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|
206
|
-
print("[DataStore
|
|
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:
|
|
327
|
+
return self:_doDataSync(self:GetNewWriter(), doMergeNewData)
|
|
211
328
|
end
|
|
212
329
|
end)
|
|
213
330
|
end
|
|
214
331
|
|
|
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
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
239
|
-
|
|
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
|
-
|
|
242
|
-
|
|
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
|
-
|
|
246
|
-
|
|
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
|
|
249
|
-
|
|
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:
|
|
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)
|
|
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
|
-
|
|
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
|
|
41
|
+
return Promise.defer(function(resolve)
|
|
42
|
+
return resolve(dataStore:Save())
|
|
43
|
+
end)
|
|
38
44
|
end))
|
|
39
45
|
|
|
40
46
|
return dataStore
|