@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.
@@ -8,6 +8,11 @@ local require = require(script.Parent.loader).load(script)
8
8
 
9
9
  local Table = require("Table")
10
10
  local DataStoreDeleteToken = require("DataStoreDeleteToken")
11
+ local Symbol = require("Symbol")
12
+ local Set = require("Set")
13
+ local DataStoreSnapshotUtils = require("DataStoreSnapshotUtils")
14
+
15
+ local UNSET_TOKEN = Symbol.named("unsetValue")
11
16
 
12
17
  local DataStoreWriter = {}
13
18
  DataStoreWriter.ClassName = "DataStoreWriter"
@@ -16,12 +21,17 @@ DataStoreWriter.__index = DataStoreWriter
16
21
  --[=[
17
22
  Constructs a new DataStoreWriter. In general, you will not use this API directly.
18
23
 
24
+ @param debugName string
19
25
  @return DataStoreWriter
20
26
  ]=]
21
- function DataStoreWriter.new()
27
+ function DataStoreWriter.new(debugName)
22
28
  local self = setmetatable({}, DataStoreWriter)
23
29
 
24
- self._rawSetData = {}
30
+ self._debugName = assert(debugName, "No debugName")
31
+ self._saveDataSnapshot = UNSET_TOKEN
32
+ self._fullBaseDataSnapshot = UNSET_TOKEN
33
+ self._userIdList = UNSET_TOKEN
34
+
25
35
  self._writers = {}
26
36
 
27
37
  return self
@@ -29,10 +39,40 @@ end
29
39
 
30
40
  --[=[
31
41
  Sets the ray data to write
32
- @param data table
42
+ @param saveDataSnapshot table | any
33
43
  ]=]
34
- function DataStoreWriter:SetRawData(data)
35
- self._rawSetData = Table.deepCopy(data)
44
+ function DataStoreWriter:SetSaveDataSnapshot(saveDataSnapshot)
45
+ assert(type(saveDataSnapshot) ~= "table" or table.isfrozen(saveDataSnapshot), "saveDataSnapshot should be frozen")
46
+
47
+ if saveDataSnapshot == DataStoreDeleteToken then
48
+ self._saveDataSnapshot = DataStoreDeleteToken
49
+ elseif type(saveDataSnapshot) == "table" then
50
+ self._saveDataSnapshot = Table.deepCopy(saveDataSnapshot)
51
+ else
52
+ self._saveDataSnapshot = saveDataSnapshot
53
+ end
54
+ end
55
+
56
+ function DataStoreWriter:GetDataToSave()
57
+ if self._saveDataSnapshot == UNSET_TOKEN then
58
+ return nil
59
+ end
60
+
61
+ return self._saveDataSnapshot
62
+ end
63
+
64
+ function DataStoreWriter:GetSubWritersMap()
65
+ return self._writers
66
+ end
67
+
68
+ function DataStoreWriter:SetFullBaseDataSnapshot(fullBaseDataSnapshot)
69
+ assert(type(fullBaseDataSnapshot) ~= "table" or table.isfrozen(fullBaseDataSnapshot), "fullBaseDataSnapshot should be frozen")
70
+
71
+ if fullBaseDataSnapshot == DataStoreDeleteToken then
72
+ error("[DataStoreWriter] - fullBaseDataSnapshot should not be a delete token")
73
+ end
74
+
75
+ self._fullBaseDataSnapshot = fullBaseDataSnapshot
36
76
  end
37
77
 
38
78
  --[=[
@@ -40,7 +80,7 @@ end
40
80
  @param name string
41
81
  @param writer DataStoreWriter
42
82
  ]=]
43
- function DataStoreWriter:AddWriter(name, writer)
83
+ function DataStoreWriter:AddSubWriter(name, writer)
44
84
  assert(type(name) == "string", "Bad name")
45
85
  assert(not self._writers[name], "Writer already exists for name")
46
86
  assert(writer, "Bad writer")
@@ -49,37 +89,207 @@ function DataStoreWriter:AddWriter(name, writer)
49
89
  end
50
90
 
51
91
  --[=[
52
- Merges the new data into the original value
92
+ Gets a sub writer
53
93
 
54
- @param original table?
55
- @return table -- The original table
94
+ @param name string
95
+ @return DataStoreWriter
56
96
  ]=]
57
- function DataStoreWriter:WriteMerge(original)
58
- original = original or {}
97
+ function DataStoreWriter:GetWriter(name)
98
+ assert(type(name) == "string", "Bad name")
99
+
100
+ return self._writers[name]
101
+ end
59
102
 
60
- for key, value in pairs(self._rawSetData) do
61
- if value == DataStoreDeleteToken then
62
- original[key] = nil
103
+ --[=[
104
+ Merges the incoming data.
105
+
106
+ Won't really perform a delete operation because we can't be sure if we were suppose to have reified this stuff or not.
107
+
108
+ @param incoming any
109
+ ]=]
110
+ function DataStoreWriter:ComputeDiffSnapshot(incoming)
111
+ assert(incoming ~= DataStoreDeleteToken, "Incoming value should not be DataStoreDeleteToken")
112
+
113
+ if type(incoming) == "table" then
114
+ local keys = Set.union(Set.fromKeys(self._writers), Set.fromKeys(incoming))
115
+
116
+ local baseSnapshot
117
+ if type(self._fullBaseDataSnapshot) == "table" then
118
+ baseSnapshot = self._fullBaseDataSnapshot
119
+ Set.unionUpdate(keys, Set.fromKeys(self._fullBaseDataSnapshot))
63
120
  else
64
- original[key] = value
121
+ baseSnapshot = {}
65
122
  end
66
- end
67
123
 
68
- for key, writer in pairs(self._writers) do
69
- if self._rawSetData[key] ~= nil then
70
- warn(("[DataStoreWriter.WriteMerge] - Overwritting key %q already saved as rawData with a writer")
71
- :format(tostring(key)))
124
+ local diffSnapshot = {}
125
+ for key, _ in pairs(keys) do
126
+ if self._writers[key] then
127
+ diffSnapshot[key] = self._writers[key]:ComputeDiffSnapshot(incoming[key])
128
+ else
129
+ diffSnapshot[key] = self:_computeValueDiff(baseSnapshot[key], incoming[key])
130
+ end
72
131
  end
73
132
 
74
- local result = writer:WriteMerge(original[key])
75
- if result == DataStoreDeleteToken then
76
- original[key] = nil
133
+ if not DataStoreSnapshotUtils.isEmptySnapshot(diffSnapshot) then
134
+ return table.freeze(diffSnapshot)
135
+ else
136
+ if next(keys) then
137
+ return nil -- No delta
138
+ else
139
+ return DataStoreDeleteToken
140
+ end
141
+ end
142
+ else
143
+ return self:_computeValueDiff(self._fullBaseDataSnapshot, incoming)
144
+ end
145
+ end
146
+
147
+ function DataStoreWriter:_computeValueDiff(original, incoming)
148
+ assert(original ~= DataStoreDeleteToken, "original cannot be DataStoreDeleteToken")
149
+ assert(incoming ~= DataStoreDeleteToken, "incoming cannot be DataStoreDeleteToken")
150
+
151
+ if original == incoming then
152
+ return nil
153
+ elseif original ~= nil and incoming == nil then
154
+ return DataStoreDeleteToken
155
+ elseif type(original) == "table" and type(incoming) == "table" then
156
+ return self:_computeTableDiff(original, incoming)
157
+ else
158
+ return incoming
159
+ end
160
+ end
161
+
162
+ function DataStoreWriter:_computeTableDiff(original, incoming)
163
+ assert(type(original) == "table", "Bad original")
164
+ assert(type(incoming) == "table", "Bad incoming")
165
+
166
+ local keys = Set.union(Set.fromKeys(original), Set.fromKeys(incoming))
167
+
168
+ local diffSnapshot = {}
169
+ for key, _ in pairs(keys) do
170
+ diffSnapshot[key] = self:_computeValueDiff(original[key], incoming[key])
171
+ end
172
+
173
+ if not DataStoreSnapshotUtils.isEmptySnapshot(diffSnapshot) then
174
+ return table.freeze(diffSnapshot)
175
+ else
176
+ if next(keys) then
177
+ return nil -- No delta
77
178
  else
78
- original[key] = result
179
+ return DataStoreDeleteToken
79
180
  end
80
181
  end
182
+ end
183
+
184
+ --[=[
185
+ Set of user ids to write with the data (only applies to top-level writer)
186
+
187
+ @param userIdList { number }
188
+ ]=]
189
+ function DataStoreWriter:SetUserIdList(userIdList)
190
+ assert(type(userIdList) == "table" or userIdList == nil, "Bad userIdList")
191
+
192
+ self._userIdList = userIdList
193
+ end
194
+
195
+ --[=[
196
+ User ids to associate with data
197
+
198
+ @return userIdList { number }
199
+ ]=]
200
+ function DataStoreWriter:GetUserIdList()
201
+ if self._userIdList == UNSET_TOKEN then
202
+ return nil
203
+ end
204
+
205
+ return self._userIdList
206
+ end
207
+
208
+ function DataStoreWriter:_writeMergeWriters(original)
209
+ local copy
210
+ if type(original) == "table" then
211
+ copy = table.clone(original)
212
+ else
213
+ copy = original
214
+ end
215
+
216
+ if next(self._writers) ~= nil then
217
+ -- Original was not a table. We need to swap to one.
218
+ if type(copy) ~= "table" then
219
+ copy = {}
220
+ end
221
+
222
+ -- Write our writers first...
223
+ for key, writer in pairs(self._writers) do
224
+ local result = writer:WriteMerge(copy[key])
225
+ if result == DataStoreDeleteToken then
226
+ copy[key] = nil
227
+ else
228
+ copy[key] = result
229
+ end
230
+ end
231
+ end
232
+
233
+ -- Write our save data next
234
+ if type(self._saveDataSnapshot) == "table" and next(self._saveDataSnapshot) ~= nil then
235
+ -- Original was not a table. We need to swap to one.
236
+ if type(copy) ~= "table" then
237
+ copy = {}
238
+ end
239
+
240
+ for key, value in pairs(self._saveDataSnapshot) do
241
+ if self._writers[key] then
242
+ warn(string.format("[DataStoreWriter._writeMergeWriters] - Overwriting key %q already saved as rawData with a writer with %q (was %q)", key, tostring(value), tostring(copy[key])))
243
+ end
244
+
245
+ if value == DataStoreDeleteToken then
246
+ copy[key] = nil
247
+ else
248
+ copy[key] = value
249
+ end
250
+ end
251
+ end
252
+
253
+ -- Handle empty table scenario..
254
+ -- This would also imply our original is nil somehow...
255
+ if type(copy) == "table" and next(copy) == nil then
256
+ if type(self._saveDataSnapshot) ~= "table" then
257
+ return nil
258
+ end
259
+ end
260
+
261
+ return copy
262
+ end
263
+
264
+ --[=[
265
+ Merges the new data into the original value
266
+
267
+ @param original any
268
+ @return any -- The original value
269
+ ]=]
270
+ function DataStoreWriter:WriteMerge(original)
271
+ -- Prioritize save value first, followed by writers, followed by original value
272
+
273
+ if self._saveDataSnapshot == DataStoreDeleteToken then
274
+ return DataStoreDeleteToken
275
+ elseif self._saveDataSnapshot == UNSET_TOKEN or self._saveDataSnapshot == nil or type(self._saveDataSnapshot) == "table" then
276
+ return self:_writeMergeWriters(original)
277
+ else
278
+ -- Save data must be a boolean or something
279
+ return self._saveDataSnapshot
280
+ end
281
+ end
282
+
283
+ function DataStoreWriter:IsCompleteWipe()
284
+ if self._saveDataSnapshot == UNSET_TOKEN then
285
+ return false
286
+ end
287
+
288
+ if self._saveDataSnapshot == DataStoreDeleteToken then
289
+ return true
290
+ end
81
291
 
82
- return original
292
+ return false
83
293
  end
84
294
 
85
295
  return DataStoreWriter
@@ -171,6 +171,7 @@ function PlayerDataStoreManager:_createDataStore(player)
171
171
  assert(not self._datastores[player], "Bad player")
172
172
 
173
173
  local datastore = DataStore.new(self._robloxDataStore, self:_getKey(player))
174
+ datastore:SetUserIdList({ player.UserId })
174
175
 
175
176
  self._maid._savingConns[player] = datastore.Saving:Connect(function(promise)
176
177
  self._pendingSaves:Add(promise)
@@ -69,13 +69,14 @@ function DataStorePromises.getAsync(robloxDataStore, key)
69
69
 
70
70
  return Promise.spawn(function(resolve, reject)
71
71
  local result = nil
72
+ local dataStoreKeyInfo = nil
72
73
  local ok, err = pcall(function()
73
- result = robloxDataStore:GetAsync(key)
74
+ result, dataStoreKeyInfo = robloxDataStore:GetAsync(key)
74
75
  end)
75
76
  if not ok then
76
77
  return reject(err)
77
78
  end
78
- return resolve(result)
79
+ return resolve(result, dataStoreKeyInfo)
79
80
  end)
80
81
  end
81
82
 
@@ -17,12 +17,16 @@ function DataStoreStringUtils.isValidUTF8(str)
17
17
  return false, "Not a string"
18
18
  end
19
19
 
20
- -- https://gist.github.com/TheGreatSageEqualToHeaven/e0e1dc2698307c93f6013b9825705899?permalink_comment_id=4334757#gistcomment-4334757
21
- if utf8.len(str) == nil then
20
+ if not utf8.len(str) then
21
+ return false, "Invalid string"
22
+ end
23
+
24
+ -- https://gist.github.com/TheGreatSageEqualToHeaven/e0e1dc2698307c93f6013b9825705899#patch-1
25
+ if string.match(str, "[^\0-\127]") then
22
26
  return false, "Invalid string"
23
27
  end
24
28
 
25
29
  return true
26
30
  end
27
31
 
28
- return DataStoreStringUtils
32
+ return DataStoreStringUtils
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "DataStoreTest",
3
+ "tree": {
4
+ "$className": "DataModel",
5
+ "ServerScriptService": {
6
+ "datastore": {
7
+ "$path": ".."
8
+ },
9
+ "Script": {
10
+ "$path": "scripts/Server"
11
+ }
12
+ },
13
+ "StarterPlayer": {
14
+ "StarterPlayerScripts": {
15
+ "Main": {
16
+ "$path": "scripts/Client"
17
+ }
18
+ }
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,10 @@
1
+ --[[
2
+ @class ClientMain
3
+ ]]
4
+
5
+ local packages = game:GetService("ReplicatedStorage"):WaitForChild("Packages")
6
+
7
+ local serviceBag = require(packages.ServiceBag).new()
8
+
9
+ serviceBag:Init()
10
+ serviceBag:Start()
@@ -0,0 +1,120 @@
1
+ --[[
2
+ @class ServerMain
3
+ ]]
4
+
5
+ local ServerScriptService = game:GetService("ServerScriptService")
6
+ local HttpService = game:GetService("HttpService")
7
+
8
+ local loader = ServerScriptService:FindFirstChild("LoaderUtils", true).Parent
9
+ local packages = require(loader).bootstrapGame(ServerScriptService.datastore)
10
+
11
+ local Maid = require(packages.Maid)
12
+ local Promise = require(packages.Promise)
13
+
14
+ local TURN_TIME = 8
15
+
16
+ local function spinUpGameCopy(prefix)
17
+ assert(type(prefix) == "string", "Bad prefix")
18
+
19
+ local serviceBag = require(packages.ServiceBag).new()
20
+ serviceBag:GetService(require(packages.GameDataStoreService))
21
+ serviceBag:GetService(require(packages.PlayerDataStoreService))
22
+
23
+ serviceBag:Init()
24
+ serviceBag:Start()
25
+
26
+ local guid = prefix .. " " .. HttpService:GenerateGUID(false)
27
+ local maid = Maid.new()
28
+
29
+ local gameDataStore = serviceBag:GetService(require(packages.GameDataStoreService))
30
+ local bindToCloseService = serviceBag:GetService(require(packages.BindToCloseService))
31
+
32
+ -- This would be an aggressive usage of this area, it probably won't scale well enough.
33
+ -- But writing some shared code or something like API keys should scale fine.
34
+ maid:GivePromise(gameDataStore:PromiseDataStore()):Then(function(dataStore)
35
+ local substore = dataStore:GetSubStore("AliveServers")
36
+ substore:Store(guid, true)
37
+
38
+ -- maid:GiveTask(dataStore:Observe():Subscribe(function(viewSnapshot)
39
+ -- print(string.format("(%s) dataStore:Observe()", prefix), viewSnapshot)
40
+ -- end))
41
+
42
+ if prefix == "blue" then
43
+ dataStore:SetDoDebugWriting(true)
44
+ dataStore:SetSyncOnSave(true)
45
+ dataStore:SetAutoSaveTimeSeconds(4)
46
+
47
+ -- maid:GiveTask(dataStore:Observe():Subscribe(function(viewSnapshot)
48
+ -- print(string.format("(%s) dataStore:Observe()", prefix), viewSnapshot)
49
+ -- end))
50
+
51
+ task.delay(4*TURN_TIME, function()
52
+ warn("Blue server is restoring data")
53
+
54
+ substore:Store(guid, true)
55
+ end)
56
+ elseif prefix == "red" then
57
+ warn(string.format("%s server is storing data", prefix))
58
+
59
+ -- dataStore:SetDoDebugWriting(true)
60
+ dataStore:SetSyncOnSave(true)
61
+ dataStore:SetAutoSaveTimeSeconds(4)
62
+ -- dataStore:Save()
63
+
64
+ task.delay(TURN_TIME, function()
65
+ warn(string.format("%s server is wiping data", prefix))
66
+
67
+ substore:Wipe()
68
+ -- dataStore:Save()
69
+
70
+ task.delay(TURN_TIME, function()
71
+ warn(string.format("%s server is adding substore data", prefix))
72
+
73
+ substore:Store(guid, {
74
+ playerCount = 5;
75
+ startTime = DateTime.now().UnixTimestamp
76
+ })
77
+ -- dataStore:Save()
78
+
79
+ task.delay(TURN_TIME, function()
80
+ warn(string.format("%s server is changing player count", prefix))
81
+ local guidStore = substore:GetSubStore(guid)
82
+ guidStore:Store("playerCount", 25)
83
+ -- dataStore:Save()
84
+ end)
85
+ end)
86
+ end)
87
+ end
88
+
89
+ -- TODO: Update some random numbers every second for a while....
90
+
91
+ -- TODO: Force saving twice
92
+
93
+ maid:GiveTask(dataStore:Observe():Subscribe(function(viewSnapshot)
94
+ print(string.format("(%s) dataStore:Observe()", prefix), viewSnapshot)
95
+ end))
96
+
97
+ -- dataStore:LoadAll():Then(function(data)
98
+ -- -- print(string.format("[%s][LoadAll] - Load all", prefix), data)
99
+ -- end)
100
+
101
+ -- local entrySubstore = substore:GetSubStore(guid)
102
+ -- entrySubstore:LoadAll():Then(function(data)
103
+ -- -- print(string.format("[%s][SUBSTORE][LoadAll] Loaded substore", prefix), data)
104
+ -- end)
105
+
106
+ -- entrySubstore:Overwrite(os.clock())
107
+
108
+ maid:GiveTask(bindToCloseService:RegisterPromiseOnCloseCallback(function()
109
+ substore:Delete(guid)
110
+ return Promise.resolved()
111
+ end))
112
+
113
+ end)
114
+
115
+ return maid
116
+ end
117
+
118
+ spinUpGameCopy("red")
119
+ spinUpGameCopy("blue")
120
+ spinUpGameCopy("green")