@quenty/datastore 7.23.1-canary.405.6fa018e.0 → 7.24.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.
@@ -4,6 +4,12 @@
4
4
  at children level. This minimizes accidently overwriting.
5
5
  The big cost here is that we may leave keys that can't be removed.
6
6
 
7
+ Layers in priority order:
8
+
9
+ 1. Save data
10
+ 2. Substores
11
+ 3. Base layer
12
+
7
13
  @server
8
14
  @class DataStoreStage
9
15
  ]=]
@@ -13,13 +19,17 @@ local require = require(script.Parent.loader).load(script)
13
19
  local BaseObject = require("BaseObject")
14
20
  local DataStoreDeleteToken = require("DataStoreDeleteToken")
15
21
  local DataStoreWriter = require("DataStoreWriter")
22
+ local GoodSignal = require("GoodSignal")
16
23
  local Maid = require("Maid")
24
+ local Observable = require("Observable")
25
+ local ObservableSubscriptionTable = require("ObservableSubscriptionTable")
17
26
  local Promise = require("Promise")
18
27
  local PromiseUtils = require("PromiseUtils")
19
- local Signal = require("Signal")
28
+ local Set = require("Set")
20
29
  local Table = require("Table")
21
- local Observable = require("Observable")
22
- local ObservableSubscriptionTable = require("ObservableSubscriptionTable")
30
+ local DataStoreSnapshotUtils = require("DataStoreSnapshotUtils")
31
+
32
+ local SLOW_INTEGRITY_CHECK_ENABLED = true
23
33
 
24
34
  local DataStoreStage = setmetatable({}, BaseObject)
25
35
  DataStoreStage.ClassName = "DataStoreStage"
@@ -28,6 +38,14 @@ DataStoreStage.__index = DataStoreStage
28
38
  --[=[
29
39
  Constructs a new DataStoreStage to load from. Prefer to use DataStore because this doesn't
30
40
  have any way to retrieve this.
41
+
42
+ See [DataStore], [GameDataStoreService], and [PlayerDataStoreService].
43
+
44
+ ```lua
45
+ -- Data store inherits from DataStoreStage
46
+ local dataStore = serviceBag:GetService(PlayerDataStoreService):PromiseDataStore(player):Yield()
47
+ ```
48
+
31
49
  @param loadName string
32
50
  @param loadParent DataStoreStage?
33
51
  @return DataStoreStage
@@ -39,136 +57,218 @@ function DataStoreStage.new(loadName, loadParent)
39
57
  self._loadName = loadName
40
58
  self._loadParent = loadParent
41
59
 
42
- self._savingCallbacks = {} -- [func, ...]
43
- self._takenKeys = {} -- [name] = true
60
+ self.Changed = GoodSignal.new() -- :Fire(viewSnapshot)
61
+ self._maid:GiveTask(self.Changed)
62
+
63
+ self.DataStored = GoodSignal.new()
64
+ self._maid:GiveTask(self.DataStored)
65
+
66
+ -- Stores the actual data loaded and synced (but not pending written data)
67
+ self._saveDataSnapshot = nil
44
68
  self._stores = {} -- [name] = dataSubStore
69
+ self._baseDataSnapshot = nil
45
70
 
46
- self._subsTable = ObservableSubscriptionTable.new()
47
- self._maid:GiveTask(self._subsTable)
71
+ -- View data
72
+ self._viewSnapshot = nil
73
+
74
+ self._savingCallbacks = {} -- [func, ...]
75
+
76
+ self._keySubscriptions = ObservableSubscriptionTable.new()
77
+ self._maid:GiveTask(self._keySubscriptions)
48
78
 
49
79
  return self
50
80
  end
51
81
 
52
- -- Also returns nil for speedyness
53
- function DataStoreStage:_promiseInvokeSavingCallbacks()
54
- if not next(self._savingCallbacks) then
55
- return nil
56
- end
82
+ --[=[
83
+ Stores the value, firing off events and queuing the item for save.
57
84
 
58
- local removingPromises = {}
59
- for _, func in pairs(self._savingCallbacks) do
60
- local result = func()
61
- if Promise.isPromise(result) then
62
- table.insert(removingPromises, result)
63
- end
64
- end
85
+ ```lua
86
+ dataStore:Store("money", 25)
87
+ ```
65
88
 
66
- for _, substore in pairs(self._stores) do
67
- local promise = substore:_promiseInvokeSavingCallbacks()
68
- if promise then
69
- table.insert(removingPromises, promise)
70
- end
89
+ @param key string | number
90
+ @param value any
91
+ ]=]
92
+ function DataStoreStage:Store(key, value)
93
+ assert(type(key) == "string", "Bad key")
94
+
95
+ if value == nil then
96
+ value = DataStoreDeleteToken
71
97
  end
72
98
 
73
- return PromiseUtils.all(removingPromises)
99
+ -- Ensure that we at least start loading (and thus the autosave loop) for write
100
+ self:PromiseViewUpToDate()
101
+
102
+ self:_storeAtKey(key, value)
74
103
  end
75
104
 
76
105
  --[=[
77
- Adds a callback to be called before save. This may return a promise.
78
- @param callback function -- May return a promise
79
- @return function -- Call to remove
80
- ]=]
81
- function DataStoreStage:AddSavingCallback(callback)
82
- assert(type(callback) == "function", "Bad callback")
106
+ Loads the data at the `key` and returns a promise with that value
83
107
 
84
- table.insert(self._savingCallbacks, callback)
108
+ ```lua
109
+ dataStore:Load():Then(function(data)
110
+ print(data)
111
+ end)
112
+ ```
85
113
 
86
- return function()
87
- if self.Destroy then
88
- self:RemoveSavingCallback(callback)
114
+ @param key string | number
115
+ @param defaultValue T?
116
+ @return Promise<T>
117
+ ]=]
118
+ function DataStoreStage:Load(key, defaultValue)
119
+ assert(type(key) == "string" or type(key) == "number", "Bad key")
120
+
121
+ return self:PromiseViewUpToDate():Then(function()
122
+ if type(self._viewSnapshot) == "table" then
123
+ local value = self._viewSnapshot[key]
124
+ if value ~= nil then
125
+ return value
126
+ else
127
+ return defaultValue
128
+ end
129
+ else
130
+ return defaultValue
89
131
  end
90
- end
132
+ end)
91
133
  end
92
134
 
93
135
  --[=[
94
- Removes a saving callback from the data store stage
95
- @param callback function
96
- ]=]
97
- function DataStoreStage:RemoveSavingCallback(callback)
98
- assert(type(callback) == "function", "Bad callback")
136
+ Promises the full content for the datastore
99
137
 
100
- local index = table.find(self._savingCallbacks, callback)
101
- if index then
102
- table.remove(self._savingCallbacks, index)
103
- end
138
+ ```lua
139
+ dataStore:LoadAll():Then(function(data)
140
+ print(data)
141
+ end)
142
+ ```
143
+
144
+ @return Promise<any>
145
+ ]=]
146
+ function DataStoreStage:LoadAll()
147
+ return self:PromiseViewUpToDate():Then(function()
148
+ return self._viewSnapshot
149
+ end)
104
150
  end
105
151
 
106
152
  --[=[
107
- Gets an event that will fire off whenever something is stored at this level
108
- @return Signal
153
+ Gets a sub-datastore that will write at the given key. This will have the same
154
+ helper methods as any other data store object.
155
+
156
+ ```lua
157
+ local dataStore = DataStore.new()
158
+
159
+ local saveslot = dataStore:GetSubStore("saveslot0")
160
+ saveslot:Store("Money", 0)
161
+ ```
162
+
163
+ @param key string | number
164
+ @return DataStoreStage
109
165
  ]=]
110
- function DataStoreStage:GetTopLevelDataStoredSignal()
111
- if self._topLevelStoreSignal then
112
- return self._topLevelStoreSignal
166
+ function DataStoreStage:GetSubStore(key)
167
+ assert(type(key) == "string" or type(key) == "number", "Bad key")
168
+
169
+ if self._stores[key] then
170
+ return self._stores[key]
113
171
  end
114
172
 
115
- self._topLevelStoreSignal = Signal.new()
116
- self._maid:GiveTask(self._topLevelStoreSignal)
117
- return self._topLevelStoreSignal
118
- end
173
+ local maid = Maid.new()
174
+ local newStore = DataStoreStage.new(key, self)
175
+ maid:GiveTask(newStore)
176
+
177
+ if type(self._baseDataSnapshot) == "table" then
178
+ local baseDataToTransfer = self._baseDataSnapshot[key]
179
+ if baseDataToTransfer ~= nil then
180
+ local newSnapshot = table.clone(self._baseDataSnapshot)
181
+ newSnapshot[key] = nil
182
+ newStore:MergeDiffSnapshot(baseDataToTransfer)
183
+ self._baseDataSnapshot = table.freeze(newSnapshot)
184
+ end
185
+ end
119
186
 
120
- --[=[
121
- Retrieves the full path of this datastore stage for diagnostic purposes.
122
- @return string
123
- ]=]
124
- function DataStoreStage:GetFullPath()
125
- if self._loadParent then
126
- return self._loadParent:GetFullPath() .. "." .. tostring(self._loadName)
127
- else
128
- return tostring(self._loadName)
187
+ -- Transfer save data to substore
188
+ if type(self._saveDataSnapshot) == "table" then
189
+ local saveDataToTransfer = self._saveDataSnapshot[key]
190
+
191
+ if saveDataToTransfer ~= nil then
192
+ local newSnapshot = table.clone(self._saveDataSnapshot)
193
+ newSnapshot[key] = nil
194
+
195
+ newStore:Overwrite(saveDataToTransfer)
196
+
197
+ if DataStoreSnapshotUtils.isEmptySnapshot(newSnapshot) then
198
+ self._saveDataSnapshot = nil
199
+ else
200
+ self._saveDataSnapshot = table.freeze(newSnapshot)
201
+ end
202
+ end
129
203
  end
204
+
205
+ self._stores[key] = newStore
206
+ self._maid[maid] = maid
207
+
208
+ maid:GiveTask(newStore.Changed:Connect(function()
209
+ self:_updateViewSnapshotAtKey(key)
210
+ end))
211
+ self:_updateViewSnapshotAtKey(key)
212
+
213
+ return newStore
130
214
  end
131
215
 
132
216
  --[=[
133
- Loads the data at the `name`.
217
+ Explicitely deletes data at the key
134
218
 
135
- @param name string | number
136
- @param defaultValue T?
137
- @return Promise<T>
219
+ @param key string | number
138
220
  ]=]
139
- function DataStoreStage:Load(name, defaultValue)
140
- assert(type(name) == "string" or type(name) == "number", "Bad name")
221
+ function DataStoreStage:Delete(key)
222
+ assert(type(key) == "string", "Bad key")
141
223
 
142
- if self._dataToSave and self._dataToSave[name] ~= nil then
143
- if self._dataToSave[name] == DataStoreDeleteToken then
144
- return Promise.resolved(defaultValue)
145
- else
146
- return Promise.resolved(self._dataToSave[name])
147
- end
148
- end
224
+ self:_storeAtKey(key, DataStoreDeleteToken)
225
+ end
149
226
 
150
- return self:_promiseLoadParentContent():Then(function(data)
151
- return self:_afterLoadGetAndApplyStagedData(name, data, defaultValue)
152
- end)
227
+ --[=[
228
+ Queues up a wipe of all values. This will completely set the data to nil.
229
+ ]=]
230
+ function DataStoreStage:Wipe()
231
+ self:Overwrite(DataStoreDeleteToken)
153
232
  end
154
233
 
155
234
  --[=[
156
235
  Observes the current value for the stage itself
157
236
 
158
- @param name string | number
237
+ If no key is passed than it will observe the whole view snapshot
238
+
239
+ @param key string | number | nil
159
240
  @param defaultValue T?
160
241
  @return Observable<T>
161
242
  ]=]
162
- function DataStoreStage:Observe(name, defaultValue)
163
- assert(type(name) == "string" or type(name) == "number", "Bad name")
243
+ function DataStoreStage:Observe(key, defaultValue)
244
+ assert(type(key) == "string" or type(key) == "number" or key == nil, "Bad key")
245
+
246
+ if key == nil then
247
+ return Observable.new(function(sub)
248
+ local maid = Maid.new()
249
+ maid:GivePromise(self:LoadAll())
250
+ :Then(function()
251
+ -- Only connect once loaded
252
+ maid:GiveTask(self.Changed:Connect(function(viewSnapshot)
253
+ sub:Fire(viewSnapshot)
254
+ end))
255
+
256
+ sub:Fire(self._viewSnapshot)
257
+ end, function(...)
258
+ sub:Fail(...)
259
+ end)
260
+
261
+ return maid
262
+ end)
263
+ end
164
264
 
165
265
  return Observable.new(function(sub)
166
266
  local maid = Maid.new()
167
267
 
168
- maid:GiveTask(self._subsTable:Observe(name):Subscribe(sub:GetFireFailComplete()))
268
+ maid:GiveTask(self._keySubscriptions:Observe(key):Subscribe(sub:GetFireFailComplete()))
169
269
 
170
270
  -- Load initially
171
- maid:GivePromise(self:Load(name, defaultValue))
271
+ maid:GivePromise(self:Load(key, defaultValue))
172
272
  :Then(function(value)
173
273
  sub:Fire(value)
174
274
  end, function(...)
@@ -179,61 +279,61 @@ function DataStoreStage:Observe(name, defaultValue)
179
279
  end)
180
280
  end
181
281
 
182
- -- Protected!
183
- function DataStoreStage:_afterLoadGetAndApplyStagedData(name, data, defaultValue)
184
- assert(type(name) == "string" or type(name) == "number", "Bad name")
282
+ --[=[
283
+ Adds a callback to be called before save. This may return a promise.
185
284
 
186
- if self._dataToSave and self._dataToSave[name] ~= nil then
187
- if self._dataToSave[name] == DataStoreDeleteToken then
188
- return defaultValue
189
- else
190
- return self._dataToSave[name]
191
- end
192
- elseif self._stores[name] then
193
- if self._stores[name]:HasWritableData() then
194
- local writer = self._stores[name]:GetNewWriter()
195
- local original = Table.deepCopy(data[name] or {})
196
- writer:WriteMerge(original)
197
- return original
198
- end
199
- end
285
+ @param callback function -- May return a promise
286
+ @return function -- Call to remove
287
+ ]=]
288
+ function DataStoreStage:AddSavingCallback(callback)
289
+ assert(type(callback) == "function", "Bad callback")
200
290
 
201
- if data[name] == nil then
202
- return defaultValue
203
- else
204
- return data[name]
291
+ table.insert(self._savingCallbacks, callback)
292
+
293
+ return function()
294
+ if self.Destroy then
295
+ self:RemoveSavingCallback(callback)
296
+ end
205
297
  end
206
298
  end
207
299
 
208
300
  --[=[
209
- Explicitely deletes data at the key
210
-
211
- @param name string | number
301
+ Removes a saving callback from the data store stage
302
+ @param callback function
212
303
  ]=]
213
- function DataStoreStage:Delete(name)
214
- assert(type(name) == "string", "Bad name")
304
+ function DataStoreStage:RemoveSavingCallback(callback)
305
+ assert(type(callback) == "function", "Bad callback")
215
306
 
216
- if self._takenKeys[name] then
217
- error(("[DataStoreStage] - Already have a writer for %q"):format(name))
307
+ local index = table.find(self._savingCallbacks, callback)
308
+ if index then
309
+ table.remove(self._savingCallbacks, index)
218
310
  end
311
+ end
312
+
313
+ --[=[
314
+ Gets an event that will fire off whenever something is stored at this level
219
315
 
220
- self:_doStore(name, DataStoreDeleteToken)
316
+ @return Signal
317
+ ]=]
318
+ function DataStoreStage:GetTopLevelDataStoredSignal()
319
+ return self.DataStored
221
320
  end
222
321
 
223
322
  --[=[
224
- Queues up a wipe of all values. Data must load before it can be wiped.
323
+ Retrieves the full path of this datastore stage for diagnostic purposes.
324
+
325
+ @return string
225
326
  ]=]
226
- function DataStoreStage:Wipe()
227
- return self._loadParent:Load(self._loadName, {})
228
- :Then(function(data)
229
- for key, _ in pairs(data) do
230
- if self._stores[key] then
231
- self._stores[key]:Wipe()
232
- else
233
- self:_doStore(key, DataStoreDeleteToken)
234
- end
235
- end
236
- end)
327
+ function DataStoreStage:GetFullPath()
328
+ if self._fullPath then
329
+ return self._fullPath
330
+ elseif self._loadParent then
331
+ self._fullPath = self._loadParent:GetFullPath() .. "." .. tostring(self._loadName)
332
+ return self._fullPath
333
+ else
334
+ self._fullPath = tostring(self._loadName)
335
+ return self._fullPath
336
+ end
237
337
  end
238
338
 
239
339
  --[=[
@@ -258,78 +358,108 @@ end
258
358
  @return Promise<{ [string]: true }>
259
359
  ]=]
260
360
  function DataStoreStage:PromiseKeySet()
261
- return self:_promiseLoadParentContent():Then(function(data)
262
- local keySet = {}
361
+ return self:PromiseViewUpToDate():Then(function()
362
+ return Set.fromKeys(self._viewSnapshot)
363
+ end)
364
+ end
263
365
 
264
- for key, value in pairs(data) do
265
- if value ~= DataStoreDeleteToken then
266
- keySet[key] = true
267
- end
268
- end
366
+ --[=[
367
+ This will always prioritize our own view of the world over
368
+ incoming data.
269
369
 
270
- if self._dataToSave then
271
- for key, value in pairs(self._dataToSave) do
272
- if value ~= DataStoreDeleteToken then
273
- keySet[key] = true
274
- end
275
- end
276
- end
370
+ :::tip
371
+ This is a helper method that helps load diff data into the data store.
372
+ :::
277
373
 
278
- -- Otherwise we assume previous data would have it
279
- for key, store in pairs(self._stores) do
280
- if store:HasWritableData() then
281
- keySet[key] = true
282
- end
283
- end
374
+ @param diffSnapshot any
375
+ ]=]
376
+ function DataStoreStage:MergeDiffSnapshot(diffSnapshot)
377
+ self:_checkIntegrity()
284
378
 
285
- return keySet
286
- end)
379
+ self._baseDataSnapshot = self:_updateStoresAndComputeBaseDataSnapshotFromDiffSnapshot(diffSnapshot)
380
+
381
+ self:_updateViewSnapshot()
382
+ self:_checkIntegrity()
287
383
  end
288
384
 
289
385
  --[=[
290
- Promises the full content for the datastore
386
+ Updates the base data to the saved / written data.
291
387
 
292
- @return Promise<any>
388
+ This will always prioritize our own view of the world over
389
+ incoming data.
390
+
391
+ @param parentWriter DataStoreWriter
293
392
  ]=]
294
- function DataStoreStage:LoadAll()
295
- return self:_promiseLoadParentContent():Then(function(data)
296
- local result = {}
393
+ function DataStoreStage:MarkDataAsSaved(parentWriter)
394
+ -- Update all children first
395
+ for key, subwriter in pairs(parentWriter:GetSubWritersMap()) do
396
+ local store = self._stores[key]
397
+ if store then
398
+ store:MarkDataAsSaved(subwriter)
399
+ else
400
+ warn("[DataStoreStage] - Store removed, but writer persists")
401
+ end
402
+ end
297
403
 
298
- for key, value in pairs(data) do
299
- if value == DataStoreDeleteToken then
300
- result[key] = nil
301
- elseif type(value) == "table" then
302
- result[key] = Table.deepCopy(value)
404
+ local dataToSave = parentWriter:GetDataToSave()
405
+ if self._saveDataSnapshot == DataStoreDeleteToken or dataToSave == DataStoreDeleteToken then
406
+ if self._saveDataSnapshot == dataToSave then
407
+ self._baseDataSnapshot = nil
408
+ self._saveDataSnapshot = nil
409
+ end
410
+ elseif type(self._saveDataSnapshot) == "table" or type(dataToSave) == "table" then
411
+ if type(self._saveDataSnapshot) == "table" and type(dataToSave) == "table" then
412
+ local newSaveSnapshot = table.clone(self._saveDataSnapshot)
413
+ local newBaseDataSnapshot
414
+ if type(self._baseDataSnapshot) == "table" then
415
+ newBaseDataSnapshot = table.clone(self._baseDataSnapshot)
303
416
  else
304
- result[key] = value
417
+ newBaseDataSnapshot = {}
305
418
  end
306
- end
307
419
 
308
- if self._dataToSave then
309
- for key, value in pairs(self._dataToSave) do
310
- if value == DataStoreDeleteToken then
311
- result[key] = nil
312
- elseif type(value) == "table" then
313
- result[key] = Table.deepCopy(value)
314
- else
315
- result[key] = value
420
+ for key, value in pairs(dataToSave) do
421
+ local ourSnapshotValue = self._saveDataSnapshot[key]
422
+ if Table.deepEquivalent(ourSnapshotValue, value) or ourSnapshotValue == nil then
423
+ -- This shouldn't fire any event because our save data is matching
424
+ newBaseDataSnapshot[key] = self:_updateStoresAndComputeBaseDataSnapshotValueFromDiffSnapshot(key, value)
425
+ newSaveSnapshot[key] = nil
316
426
  end
317
427
  end
318
- end
319
428
 
320
- for key, store in pairs(self._stores) do
321
- if store:HasWritableData() then
322
- local writer = store:GetNewWriter()
323
- local original = Table.deepCopy(result[key] or {})
324
- writer:WriteMerge(original)
429
+ self._baseDataSnapshot = table.freeze(newBaseDataSnapshot)
430
+
431
+ if DataStoreSnapshotUtils.isEmptySnapshot(newSaveSnapshot) then
432
+ self._saveDataSnapshot = nil
433
+ else
434
+ self._saveDataSnapshot = table.freeze(newSaveSnapshot)
325
435
  end
326
436
  end
437
+ else
438
+ assert(type(self._saveDataSnapshot) ~= "table", "Case is covered above")
439
+ assert(self._saveDataSnapshot ~= DataStoreDeleteToken, "Case is covered above")
440
+ assert(dataToSave ~= DataStoreDeleteToken, "Case is covered above")
441
+ assert(type(dataToSave) ~= "table", "Case is covered above")
442
+
443
+ -- In the none-table scenario move stuff
444
+ if self._saveDataSnapshot == dataToSave then
445
+ self._baseDataSnapshot = dataToSave
446
+ self._saveDataSnapshot = nil
447
+ end
448
+ end
327
449
 
328
- return result
329
- end)
450
+ self:_checkIntegrity()
330
451
  end
331
452
 
332
- function DataStoreStage:_promiseLoadParentContent()
453
+ --[=[
454
+ Helper method that when invokes ensures the data view.
455
+
456
+ :::tip
457
+ This is a helper method. You probably want [DataStore.LoadAll] instead.
458
+ :::
459
+
460
+ @return Promise
461
+ ]=]
462
+ function DataStoreStage:PromiseViewUpToDate()
333
463
  if not self._loadParent then
334
464
  error("[DataStoreStage.Load] - Failed to load, no loadParent!")
335
465
  end
@@ -337,54 +467,85 @@ function DataStoreStage:_promiseLoadParentContent()
337
467
  error("[DataStoreStage.Load] - Failed to load, no loadName!")
338
468
  end
339
469
 
340
- return self._loadParent:Load(self._loadName, {})
470
+ return self._loadParent:PromiseViewUpToDate()
341
471
  end
342
472
 
343
473
  --[=[
344
- Stores the value, firing off events and queuing the item
345
- for save.
474
+ Ovewrites the full stage with the data specified.
346
475
 
347
- @param name string | number
348
- @param value string
476
+ :::tip
477
+ Use this method carefully as it can lead to data loss in ways that a specific :Store() call
478
+ on the right stage would do better.
479
+ :::
480
+
481
+ @param data any
349
482
  ]=]
350
- function DataStoreStage:Store(name, value)
351
- assert(type(name) == "string", "Bad name")
483
+ function DataStoreStage:Overwrite(data)
484
+ -- Ensure that we at least start loading (and thus the autosave loop) for write
485
+ self:PromiseViewUpToDate()
352
486
 
353
- if self._takenKeys[name] then
354
- error(("[DataStoreStage] - Already have a writer for %q"):format(name))
487
+ if data == nil then
488
+ data = DataStoreDeleteToken
355
489
  end
356
490
 
357
- if value == nil then
358
- value = DataStoreDeleteToken
359
- end
491
+ if type(data) == "table" then
492
+ local newSaveSnapshot = {}
360
493
 
361
- self:_doStore(name, value)
362
- end
494
+ local remaining = Set.fromKeys(self._stores)
495
+ for key, store in pairs(self._stores) do
496
+ -- Update each store
497
+ store:Overwrite(data[key])
498
+ end
363
499
 
364
- --[=[
365
- Gets a sub-datastore that will write at the given name point
500
+ for key, value in pairs(data) do
501
+ remaining[key] = nil
502
+ if self._stores[key] then
503
+ self._stores[key]:Overwrite(value)
504
+ else
505
+ newSaveSnapshot[key] = value
506
+ end
507
+ end
366
508
 
367
- @param name string | number
368
- @return DataStoreStage
369
- ]=]
370
- function DataStoreStage:GetSubStore(name)
371
- assert(type(name) == "string" or type(name) == "number", "Bad name")
509
+ for key, _ in pairs(remaining) do
510
+ self._stores[key]:Overwrite(DataStoreDeleteToken)
511
+ end
372
512
 
373
- if self._stores[name] then
374
- return self._stores[name]
375
- end
513
+ self._saveDataSnapshot = table.freeze(newSaveSnapshot)
514
+ else
515
+ for _, store in pairs(self._stores) do
516
+ store:Overwrite(DataStoreDeleteToken)
517
+ end
376
518
 
377
- if self._takenKeys[name] then
378
- error(("[DataStoreStage.GetSubStore] - Already have a writer for %q"):format(name))
519
+ self._saveDataSnapshot = data
379
520
  end
380
521
 
381
- local newStore = DataStoreStage.new(name, self)
382
- self._takenKeys[name] = true
383
- self._maid:GiveTask(newStore)
522
+ self:_updateViewSnapshot()
523
+ end
524
+
525
+ --[=[
526
+ Ovewrites the full stage with the data specified. However, it will merge the data
527
+ to help prevent data-loss.
384
528
 
385
- self._stores[name] = newStore
529
+ :::tip
530
+ Use this method carefully as it can lead to data loss in ways that a specific :Store() call
531
+ on the right stage would do better.
532
+ :::
386
533
 
387
- return newStore
534
+ @param data any
535
+ ]=]
536
+ function DataStoreStage:OverwriteMerge(data)
537
+ -- Ensure that we at least start loading (and thus the autosave loop) for write
538
+ self:PromiseViewUpToDate()
539
+
540
+ if type(data) == "table" and data ~= DataStoreDeleteToken then
541
+ -- Note we explicitly don't wipe values here! Need delete token if we want to delete!
542
+ for key, value in pairs(data) do
543
+ self:_storeAtKey(key, value)
544
+ end
545
+ else
546
+ -- Non-tables
547
+ self:Overwrite(data)
548
+ end
388
549
  end
389
550
 
390
551
  --[=[
@@ -398,19 +559,10 @@ function DataStoreStage:StoreOnValueChange(name, valueObj)
398
559
  assert(type(name) == "string" or type(name) == "number", "Bad name")
399
560
  assert(typeof(valueObj) == "Instance" or (type(valueObj) == "table" and valueObj.Changed), "Bad valueObj")
400
561
 
401
- if self._takenKeys[name] then
402
- error(("[DataStoreStage] - Already have a writer for %q"):format(name))
403
- end
404
-
405
562
  local maid = Maid.new()
406
563
 
407
- self._takenKeys[name] = true
408
- maid:GiveTask(function()
409
- self._takenKeys[name] = nil
410
- end)
411
-
412
564
  maid:GiveTask(valueObj.Changed:Connect(function()
413
- self:_doStore(name, valueObj.Value)
565
+ self:_storeAtKey(name, valueObj.Value)
414
566
  end))
415
567
 
416
568
  return maid
@@ -422,7 +574,7 @@ end
422
574
  @return boolean
423
575
  ]=]
424
576
  function DataStoreStage:HasWritableData()
425
- if self._dataToSave then
577
+ if self._saveDataSnapshot ~= nil then
426
578
  return true
427
579
  end
428
580
 
@@ -441,57 +593,403 @@ function DataStoreStage:HasWritableData()
441
593
  end
442
594
 
443
595
  --[=[
444
- Constructs a writer which provides a snapshot of the current data state to write
596
+ Constructs a writer which provides a snapshot of the current data state to write.
597
+
598
+ :::tip
599
+ This is automatically invoked during saving and is public so [DataStore] can invoke it.
600
+ :::
445
601
 
446
602
  @return DataStoreWriter
447
603
  ]=]
448
604
  function DataStoreStage:GetNewWriter()
449
- local writer = DataStoreWriter.new()
450
- if self._dataToSave then
451
- writer:SetRawData(self._dataToSave)
605
+ self:_checkIntegrity()
606
+
607
+ local writer = DataStoreWriter.new(self:GetFullPath())
608
+
609
+ local fullBaseDataSnapshot = self:_createFullBaseDataSnapshot()
610
+
611
+ if self._saveDataSnapshot ~= nil then
612
+ writer:SetSaveDataSnapshot(self._saveDataSnapshot)
452
613
  end
453
614
 
454
- for name, store in pairs(self._stores) do
615
+ for key, store in pairs(self._stores) do
455
616
  if not store.Destroy then
456
- warn(("[DataStoreStage] - Substore %q destroyed"):format(name))
617
+ warn(("[DataStoreStage] - Substore %q destroyed"):format(key))
457
618
  continue
458
619
  end
459
620
 
460
621
  if store:HasWritableData() then
461
- writer:AddWriter(name, store:GetNewWriter())
622
+ writer:AddSubWriter(key, store:GetNewWriter())
462
623
  end
463
624
  end
464
625
 
626
+ writer:SetFullBaseDataSnapshot(fullBaseDataSnapshot)
627
+
465
628
  return writer
466
629
  end
467
630
 
631
+ --[=[
632
+ Invokes all saving callbacks
633
+
634
+ :::tip
635
+ This is automatically invoked before saving and is public so [DataStore] can invoke it.
636
+ :::
637
+
638
+ @return Promise
639
+ ]=]
640
+ function DataStoreStage:PromiseInvokeSavingCallbacks()
641
+ if not next(self._savingCallbacks) then
642
+ return Promise.resolved()
643
+ end
644
+
645
+ local removingPromises = {}
646
+ for _, func in pairs(self._savingCallbacks) do
647
+ local result = func()
648
+ if Promise.isPromise(result) then
649
+ table.insert(removingPromises, result)
650
+ end
651
+ end
652
+
653
+ for _, substore in pairs(self._stores) do
654
+ local promise = substore:PromiseInvokeSavingCallbacks()
655
+ if promise then
656
+ table.insert(removingPromises, promise)
657
+ end
658
+ end
659
+
660
+ return PromiseUtils.all(removingPromises)
661
+ end
662
+
663
+ function DataStoreStage:_createFullBaseDataSnapshot()
664
+ if self._baseDataSnapshot == DataStoreDeleteToken then
665
+ error("BadDataSnapshot cannot be a delete token")
666
+ elseif type(self._baseDataSnapshot) == "table" or self._baseDataSnapshot == nil then
667
+ local newSnapshot
668
+ if type(self._baseDataSnapshot) == "table" then
669
+ newSnapshot = table.clone(self._baseDataSnapshot)
670
+ else
671
+ newSnapshot = {}
672
+ end
673
+
674
+ for key, store in pairs(self._stores) do
675
+ if not store.Destroy then
676
+ warn(("[DataStoreStage] - Substore %q destroyed"):format(key))
677
+ continue
678
+ end
679
+
680
+ if not store:HasWritableData() then
681
+ newSnapshot[key] = store:_createFullBaseDataSnapshot()
682
+ end
683
+ end
684
+
685
+ if DataStoreSnapshotUtils.isEmptySnapshot(newSnapshot) then
686
+ return nil
687
+ else
688
+ return table.freeze(newSnapshot)
689
+ end
690
+ else
691
+ return self._baseDataSnapshot
692
+ end
693
+ end
694
+
695
+ function DataStoreStage:_updateStoresAndComputeBaseDataSnapshotFromDiffSnapshot(diffSnapshot)
696
+ if diffSnapshot == DataStoreDeleteToken then
697
+ return nil
698
+ elseif type(diffSnapshot) == "table" then
699
+ local newBaseDataSnapshot
700
+ if type(self._baseDataSnapshot) == "table" then
701
+ newBaseDataSnapshot = table.clone(self._baseDataSnapshot)
702
+ else
703
+ newBaseDataSnapshot = {}
704
+ end
705
+
706
+ -- Merge all of our newly downloaded data here into our base layer.
707
+ for key, value in pairs(diffSnapshot) do
708
+ newBaseDataSnapshot[key] = self:_updateStoresAndComputeBaseDataSnapshotValueFromDiffSnapshot(key, value)
709
+ end
710
+
711
+ return table.freeze(newBaseDataSnapshot)
712
+ elseif diffSnapshot == nil then
713
+ return self._baseDataSnapshot
714
+ else
715
+ return diffSnapshot
716
+ end
717
+ end
718
+
719
+ function DataStoreStage:_updateStoresAndComputeBaseDataSnapshotValueFromDiffSnapshot(key, value)
720
+ assert(type(key) == "string" or type(key) == "number", "Bad key")
721
+
722
+ if self._stores[key] then
723
+ self._stores[key]:MergeDiffSnapshot(value)
724
+ return nil
725
+ elseif value == DataStoreDeleteToken then
726
+ return nil
727
+ elseif type(value) == "table" and type(self._baseDataSnapshot) == "table" and type(self._baseDataSnapshot[key]) == "table" then
728
+ return self:_recurseMergeTable(self._baseDataSnapshot[key], value)
729
+ else
730
+ return value
731
+ end
732
+ end
733
+
734
+ function DataStoreStage:_recurseMergeTable(original, incoming)
735
+ if incoming == DataStoreDeleteToken then
736
+ return nil
737
+ elseif type(incoming) == "table" and type(original) == "table" then
738
+ -- Merge
739
+ local newSnapshot = table.clone(original)
740
+
741
+ -- Overwerite with merged values...
742
+ for key, value in pairs(incoming) do
743
+ newSnapshot[key] = self:_recurseMergeTable(original[key], value)
744
+ end
745
+
746
+ return table.freeze(newSnapshot)
747
+ else
748
+ return incoming
749
+ end
750
+ end
751
+
752
+ function DataStoreStage:_updateViewSnapshot()
753
+ self:_checkIntegrity()
754
+
755
+ local newViewSnapshot = self:_computeNewViewSnapshot()
756
+
757
+ if not Table.deepEquivalent(self._viewSnapshot, newViewSnapshot) then
758
+ local previousViewSnapshot = self._viewSnapshot
759
+ self._viewSnapshot = newViewSnapshot
760
+
761
+ -- Fire off changed keys
762
+ local changedKeys = self:_computeChangedKeys(previousViewSnapshot, newViewSnapshot)
763
+ if next(changedKeys) ~= nil then
764
+ if type(newViewSnapshot) == "table" then
765
+ for key, _ in pairs(changedKeys) do
766
+ self._keySubscriptions:Fire(key, newViewSnapshot[key])
767
+ end
768
+ else
769
+ for key, _ in pairs(changedKeys) do
770
+ self._keySubscriptions:Fire(key, nil)
771
+ end
772
+ end
773
+ end
774
+
775
+ self.Changed:Fire(self._viewSnapshot)
776
+ end
777
+
778
+ self:_checkIntegrity()
779
+ end
780
+
781
+ function DataStoreStage:_computeChangedKeys(previousViewSnapshot, newViewSnapshot)
782
+ -- Detect keys that changed
783
+ if type(previousViewSnapshot) == "table" and type(newViewSnapshot) == "table" then
784
+ local changedKeys = {}
785
+
786
+ local keys = Set.union(Set.fromKeys(previousViewSnapshot), Set.fromKeys(newViewSnapshot))
787
+ for key, _ in pairs(keys) do
788
+ if not Table.deepEquivalent(previousViewSnapshot[key], newViewSnapshot[key]) then
789
+ changedKeys[key] = true
790
+ end
791
+ end
792
+
793
+ return changedKeys
794
+ elseif type(newViewSnapshot) == "table" then
795
+ -- Swap to table, all keys change
796
+ return Set.fromKeys(newViewSnapshot)
797
+ elseif type(previousViewSnapshot) == "table" then
798
+ -- Swap from table, all keys change
799
+ return Set.fromKeys(previousViewSnapshot)
800
+ else
801
+ return {}
802
+ end
803
+ end
804
+
805
+ function DataStoreStage:_updateViewSnapshotAtKey(key)
806
+ assert(type(key) == "string" or type(key) == "number", "Bad key")
807
+
808
+ if type(self._viewSnapshot) ~= "table" then
809
+ self:_updateViewSnapshot()
810
+ return
811
+ end
812
+
813
+ local newValue = self:_computeViewValueForKey(key)
814
+ if self._viewSnapshot[key] == newValue then
815
+ return
816
+ end
817
+
818
+ local newSnapshot = table.clone(self._viewSnapshot)
819
+ newSnapshot[key] = newValue
820
+
821
+
822
+ self._viewSnapshot = table.freeze(newSnapshot)
823
+ self._keySubscriptions:Fire(key, newValue)
824
+ self.Changed:Fire(self._viewSnapshot)
825
+
826
+ self:_checkIntegrity()
827
+ end
828
+
829
+ function DataStoreStage:_computeNewViewSnapshot()
830
+ -- This prioritizes save data first, then stores, then base data
831
+
832
+ if self._saveDataSnapshot == DataStoreDeleteToken then
833
+ return nil
834
+ elseif self._saveDataSnapshot == nil or type(self._saveDataSnapshot) == "table" then
835
+ -- Compute a new view
836
+
837
+ -- Start with base data
838
+ local newView
839
+ if type(self._baseDataSnapshot) == "table" then
840
+ newView = table.clone(self._baseDataSnapshot)
841
+ else
842
+ newView = {}
843
+ end
844
+
845
+ -- Add in stores
846
+ for key, store in pairs(self._stores) do
847
+ newView[key] = store._viewSnapshot
848
+ end
849
+
850
+ -- Then finally save data
851
+ if type(self._saveDataSnapshot) == "table" then
852
+ for key, value in pairs(self._saveDataSnapshot) do
853
+ if value == DataStoreDeleteToken then
854
+ newView[key] = nil
855
+ else
856
+ newView[key] = value
857
+ end
858
+ end
859
+ end
860
+
861
+ if next(newView) == nil and not (type(self._baseDataSnapshot) == "table" or type(self._saveDataSnapshot) == "table") then
862
+ -- We haev no reason to be a table, make sure we return nil
863
+ return nil
864
+ end
865
+
866
+ return table.freeze(newView)
867
+ else
868
+ assert(self._saveDataSnapshot ~= nil, "Bad _saveDataSnapshot")
869
+ assert(type(self._saveDataSnapshot) ~= "table", "Bad self._saveDataSnapshot")
870
+
871
+ -- If save data isn't nil or a table then we are to return the save table
872
+ return self._saveDataSnapshot
873
+ end
874
+ end
875
+
876
+ function DataStoreStage:_computeViewValueForKey(key)
877
+ -- This prioritizes save data first, then stores, then base data
878
+
879
+ if self._saveDataSnapshot == DataStoreDeleteToken then
880
+ return nil
881
+ elseif self._saveDataSnapshot == nil or type(self._saveDataSnapshot) == "table" then
882
+ if type(self._saveDataSnapshot) == "table" then
883
+ if self._saveDataSnapshot[key] ~= nil then
884
+ local value = self._saveDataSnapshot[key]
885
+ if value == DataStoreDeleteToken then
886
+ return nil
887
+ else
888
+ return value
889
+ end
890
+ end
891
+ end
892
+
893
+ if self._stores[key] then
894
+ local value = self._stores[key]._viewSnapshot
895
+ if value == DataStoreDeleteToken then
896
+ return nil
897
+ else
898
+ return value
899
+ end
900
+ end
901
+
902
+ if type(self._baseDataSnapshot) == "table" then
903
+ if self._baseDataSnapshot[key] ~= nil then
904
+ return self._baseDataSnapshot[key]
905
+ end
906
+ end
907
+
908
+ return nil
909
+ else
910
+ -- If save data isn't nil or a table then we are to return nil.
911
+ return nil
912
+ end
913
+ end
914
+
468
915
  -- Stores the data for overwrite.
469
- function DataStoreStage:_doStore(name, value)
470
- assert(type(name) == "string" or type(name) == "number", "Bad name")
916
+ function DataStoreStage:_storeAtKey(key, value)
917
+ assert(type(key) == "string" or type(key) == "number", "Bad key")
471
918
  assert(value ~= nil, "Bad value")
472
919
 
473
- local newValue
474
- if value == DataStoreDeleteToken then
475
- newValue = DataStoreDeleteToken
476
- elseif type(value) == "table" then
477
- newValue = Table.deepCopy(value)
920
+ local deepClonedSaveValue
921
+ if type(value) == "table" then
922
+ deepClonedSaveValue = table.freeze(Table.deepCopy(value))
478
923
  else
479
- newValue = value
924
+ deepClonedSaveValue = value
480
925
  end
481
926
 
482
- if not self._dataToSave then
483
- self._dataToSave = {}
927
+ if self._stores[key] then
928
+ self._stores[key]:Overwrite(value)
929
+ return
484
930
  end
485
931
 
486
- self._dataToSave[name] = newValue
487
- if self._topLevelStoreSignal then
488
- self._topLevelStoreSignal:Fire()
932
+ local swappedSaveSnapshotType = false
933
+ local newSnapshot
934
+
935
+ if type(self._saveDataSnapshot) == "table" then
936
+ newSnapshot = table.clone(self._saveDataSnapshot)
937
+ else
938
+ swappedSaveSnapshotType = true
939
+ newSnapshot = {}
489
940
  end
490
941
 
491
- if newValue == DataStoreDeleteToken then
492
- self._subsTable:Fire(name, nil)
942
+ newSnapshot[key] = deepClonedSaveValue
943
+
944
+ self._saveDataSnapshot = table.freeze(newSnapshot)
945
+
946
+ self.DataStored:Fire()
947
+
948
+ if swappedSaveSnapshotType then
949
+ self:_updateViewSnapshot()
493
950
  else
494
- self._subsTable:Fire(name, newValue)
951
+ self:_updateViewSnapshotAtKey(key)
952
+ end
953
+ self:_checkIntegrity()
954
+ end
955
+
956
+ function DataStoreStage:_checkIntegrity()
957
+ if not SLOW_INTEGRITY_CHECK_ENABLED then
958
+ return
959
+ end
960
+
961
+ assert(self._baseDataSnapshot ~= DataStoreDeleteToken, "BaseDataSnapshot should not be DataStoreDeleteToken")
962
+ assert(self._viewSnapshot ~= DataStoreDeleteToken, "ViewSnapshot should not be DataStoreDeleteToken")
963
+
964
+ if type(self._baseDataSnapshot) == "table" then
965
+ assert(table.isfrozen(self._baseDataSnapshot), "Base snapshot should be frozen")
966
+ end
967
+
968
+ if type(self._saveDataSnapshot) == "table" then
969
+ assert(table.isfrozen(self._saveDataSnapshot), "Save snapshot should be frozen")
970
+ end
971
+
972
+ if type(self._viewSnapshot) == "table" then
973
+ assert(table.isfrozen(self._viewSnapshot), "View snapshot should be frozen")
974
+ end
975
+
976
+ for key, _ in pairs(self._stores) do
977
+ if type(self._baseDataSnapshot) == "table" and self._baseDataSnapshot[key] ~= nil then
978
+ error(string.format("[DataStoreStage] - Duplicate baseData at key %q", key))
979
+ end
980
+
981
+ if type(self._saveDataSnapshot) == "table" and self._saveDataSnapshot[key] ~= nil then
982
+ error(string.format("[DataStoreStage] - Duplicate saveData at key %q", key))
983
+ end
984
+ end
985
+
986
+ if type(self._viewSnapshot) == "table" then
987
+ for key, value in pairs(self._viewSnapshot) do
988
+ assert(type(key) == "string" or type(key) == "number", "Bad key")
989
+ if value == DataStoreDeleteToken then
990
+ error(string.format("[DataStoreStage] - View at key %q is delete token", key))
991
+ end
992
+ end
495
993
  end
496
994
  end
497
995