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