@quenty/datastore 8.0.0-canary.367.e9fdcbc.0 → 8.0.1-canary.397d174.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
331
+
332
+ --[=[
333
+ Gets an event that will fire off whenever something is stored at this level
219
334
 
220
- self:_doStore(name, DataStoreDeleteToken)
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,112 @@ end
258
377
  @return Promise<{ [string]: true }>
259
378
  ]=]
260
379
  function DataStoreStage:PromiseKeySet()
261
- return self:_promiseLoadParentContent():Then(function(data)
262
- local keySet = {}
263
-
264
- for key, value in pairs(data) do
265
- if value ~= DataStoreDeleteToken then
266
- keySet[key] = true
267
- end
380
+ return self:PromiseViewUpToDate():Then(function()
381
+ if type(self._viewSnapshot) == "table" then
382
+ return Set.fromKeys(self._viewSnapshot)
383
+ else
384
+ return {}
268
385
  end
386
+ end)
387
+ end
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
+ --[=[
390
+ This will always prioritize our own view of the world over
391
+ incoming data.
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
+ :::tip
394
+ This is a helper method that helps load diff data into the data store.
395
+ :::
284
396
 
285
- return keySet
286
- end)
397
+ @param diffSnapshot any
398
+ ]=]
399
+ function DataStoreStage:MergeDiffSnapshot(diffSnapshot)
400
+ self:_checkIntegrity()
401
+
402
+ self._baseDataSnapshot = self:_updateStoresAndComputeBaseDataSnapshotFromDiffSnapshot(diffSnapshot)
403
+
404
+ self:_updateViewSnapshot()
405
+ self:_checkIntegrity()
287
406
  end
288
407
 
289
408
  --[=[
290
- Promises the full content for the datastore
409
+ Updates the base data to the saved / written data.
291
410
 
292
- @return Promise<any>
411
+ This will always prioritize our own view of the world over
412
+ incoming data.
413
+
414
+ @param parentWriter DataStoreWriter
293
415
  ]=]
294
- function DataStoreStage:LoadAll()
295
- return self:_promiseLoadParentContent():Then(function(data)
296
- local result = {}
416
+ function DataStoreStage:MarkDataAsSaved(parentWriter)
417
+ -- Update all children first
418
+ for key, subwriter in pairs(parentWriter:GetSubWritersMap()) do
419
+ local store = self._stores[key]
420
+ if store then
421
+ store:MarkDataAsSaved(subwriter)
422
+ else
423
+ warn("[DataStoreStage] - Store removed, but writer persists")
424
+ end
425
+ end
297
426
 
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)
427
+ local dataToSave = parentWriter:GetDataToSave()
428
+ if self._saveDataSnapshot == DataStoreDeleteToken or dataToSave == DataStoreDeleteToken then
429
+ if self._saveDataSnapshot == dataToSave then
430
+ self._baseDataSnapshot = nil
431
+ self._saveDataSnapshot = nil
432
+ end
433
+ elseif type(self._saveDataSnapshot) == "table" or type(dataToSave) == "table" then
434
+ if type(self._saveDataSnapshot) == "table" and type(dataToSave) == "table" then
435
+ local newSaveSnapshot = table.clone(self._saveDataSnapshot)
436
+ local newBaseDataSnapshot
437
+ if type(self._baseDataSnapshot) == "table" then
438
+ newBaseDataSnapshot = table.clone(self._baseDataSnapshot)
303
439
  else
304
- result[key] = value
440
+ newBaseDataSnapshot = {}
305
441
  end
306
- end
307
442
 
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
443
+ for key, value in pairs(dataToSave) do
444
+ local ourSnapshotValue = self._saveDataSnapshot[key]
445
+ if Table.deepEquivalent(ourSnapshotValue, value) or ourSnapshotValue == nil then
446
+ -- This shouldn't fire any event because our save data is matching
447
+ newBaseDataSnapshot[key] = self:_updateStoresAndComputeBaseDataSnapshotValueFromDiffSnapshot(key, value)
448
+ newSaveSnapshot[key] = nil
316
449
  end
317
450
  end
318
- end
319
451
 
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)
452
+ self._baseDataSnapshot = table.freeze(newBaseDataSnapshot)
453
+
454
+ if DataStoreSnapshotUtils.isEmptySnapshot(newSaveSnapshot) then
455
+ self._saveDataSnapshot = nil
456
+ else
457
+ self._saveDataSnapshot = table.freeze(newSaveSnapshot)
325
458
  end
326
459
  end
460
+ else
461
+ assert(type(self._saveDataSnapshot) ~= "table", "Case is covered above")
462
+ assert(self._saveDataSnapshot ~= DataStoreDeleteToken, "Case is covered above")
463
+ assert(dataToSave ~= DataStoreDeleteToken, "Case is covered above")
464
+ assert(type(dataToSave) ~= "table", "Case is covered above")
465
+
466
+ -- In the none-table scenario move stuff
467
+ if self._saveDataSnapshot == dataToSave then
468
+ self._baseDataSnapshot = dataToSave
469
+ self._saveDataSnapshot = nil
470
+ end
471
+ end
327
472
 
328
- return result
329
- end)
473
+ self:_checkIntegrity()
330
474
  end
331
475
 
332
- function DataStoreStage:_promiseLoadParentContent()
476
+ --[=[
477
+ Helper method that when invokes ensures the data view.
478
+
479
+ :::tip
480
+ This is a helper method. You probably want [DataStore.LoadAll] instead.
481
+ :::
482
+
483
+ @return Promise
484
+ ]=]
485
+ function DataStoreStage:PromiseViewUpToDate()
333
486
  if not self._loadParent then
334
487
  error("[DataStoreStage.Load] - Failed to load, no loadParent!")
335
488
  end
@@ -337,54 +490,85 @@ function DataStoreStage:_promiseLoadParentContent()
337
490
  error("[DataStoreStage.Load] - Failed to load, no loadName!")
338
491
  end
339
492
 
340
- return self._loadParent:Load(self._loadName, {})
493
+ return self._loadParent:PromiseViewUpToDate()
341
494
  end
342
495
 
343
496
  --[=[
344
- Stores the value, firing off events and queuing the item
345
- for save.
497
+ Ovewrites the full stage with the data specified.
346
498
 
347
- @param name string | number
348
- @param value string
499
+ :::tip
500
+ Use this method carefully as it can lead to data loss in ways that a specific :Store() call
501
+ on the right stage would do better.
502
+ :::
503
+
504
+ @param data any
349
505
  ]=]
350
- function DataStoreStage:Store(name, value)
351
- assert(type(name) == "string", "Bad name")
506
+ function DataStoreStage:Overwrite(data)
507
+ -- Ensure that we at least start loading (and thus the autosave loop) for write
508
+ self:PromiseViewUpToDate()
352
509
 
353
- if self._takenKeys[name] then
354
- error(("[DataStoreStage] - Already have a writer for %q"):format(name))
510
+ if data == nil then
511
+ data = DataStoreDeleteToken
355
512
  end
356
513
 
357
- if value == nil then
358
- value = DataStoreDeleteToken
359
- end
514
+ if type(data) == "table" then
515
+ local newSaveSnapshot = {}
360
516
 
361
- self:_doStore(name, value)
362
- end
517
+ local remaining = Set.fromKeys(self._stores)
518
+ for key, store in pairs(self._stores) do
519
+ -- Update each store
520
+ store:Overwrite(data[key])
521
+ end
363
522
 
364
- --[=[
365
- Gets a sub-datastore that will write at the given name point
523
+ for key, value in pairs(data) do
524
+ remaining[key] = nil
525
+ if self._stores[key] then
526
+ self._stores[key]:Overwrite(value)
527
+ else
528
+ newSaveSnapshot[key] = value
529
+ end
530
+ end
366
531
 
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")
532
+ for key, _ in pairs(remaining) do
533
+ self._stores[key]:Overwrite(DataStoreDeleteToken)
534
+ end
372
535
 
373
- if self._stores[name] then
374
- return self._stores[name]
375
- end
536
+ self._saveDataSnapshot = table.freeze(newSaveSnapshot)
537
+ else
538
+ for _, store in pairs(self._stores) do
539
+ store:Overwrite(DataStoreDeleteToken)
540
+ end
376
541
 
377
- if self._takenKeys[name] then
378
- error(("[DataStoreStage.GetSubStore] - Already have a writer for %q"):format(name))
542
+ self._saveDataSnapshot = data
379
543
  end
380
544
 
381
- local newStore = DataStoreStage.new(name, self)
382
- self._takenKeys[name] = true
383
- self._maid:GiveTask(newStore)
545
+ self:_updateViewSnapshot()
546
+ end
384
547
 
385
- self._stores[name] = newStore
548
+ --[=[
549
+ Ovewrites the full stage with the data specified. However, it will merge the data
550
+ to help prevent data-loss.
386
551
 
387
- return newStore
552
+ :::tip
553
+ Use this method carefully as it can lead to data loss in ways that a specific :Store() call
554
+ on the right stage would do better.
555
+ :::
556
+
557
+ @param data any
558
+ ]=]
559
+ function DataStoreStage:OverwriteMerge(data)
560
+ -- Ensure that we at least start loading (and thus the autosave loop) for write
561
+ self:PromiseViewUpToDate()
562
+
563
+ if type(data) == "table" and data ~= DataStoreDeleteToken then
564
+ -- Note we explicitly don't wipe values here! Need delete token if we want to delete!
565
+ for key, value in pairs(data) do
566
+ self:_storeAtKey(key, value)
567
+ end
568
+ else
569
+ -- Non-tables
570
+ self:Overwrite(data)
571
+ end
388
572
  end
389
573
 
390
574
  --[=[
@@ -398,19 +582,10 @@ function DataStoreStage:StoreOnValueChange(name, valueObj)
398
582
  assert(type(name) == "string" or type(name) == "number", "Bad name")
399
583
  assert(typeof(valueObj) == "Instance" or (type(valueObj) == "table" and valueObj.Changed), "Bad valueObj")
400
584
 
401
- if self._takenKeys[name] then
402
- error(("[DataStoreStage] - Already have a writer for %q"):format(name))
403
- end
404
-
405
585
  local maid = Maid.new()
406
586
 
407
- self._takenKeys[name] = true
408
- maid:GiveTask(function()
409
- self._takenKeys[name] = nil
410
- end)
411
-
412
587
  maid:GiveTask(valueObj.Changed:Connect(function()
413
- self:_doStore(name, valueObj.Value)
588
+ self:_storeAtKey(name, valueObj.Value)
414
589
  end))
415
590
 
416
591
  return maid
@@ -422,7 +597,7 @@ end
422
597
  @return boolean
423
598
  ]=]
424
599
  function DataStoreStage:HasWritableData()
425
- if self._dataToSave then
600
+ if self._saveDataSnapshot ~= nil then
426
601
  return true
427
602
  end
428
603
 
@@ -441,57 +616,403 @@ function DataStoreStage:HasWritableData()
441
616
  end
442
617
 
443
618
  --[=[
444
- Constructs a writer which provides a snapshot of the current data state to write
619
+ Constructs a writer which provides a snapshot of the current data state to write.
620
+
621
+ :::tip
622
+ This is automatically invoked during saving and is public so [DataStore] can invoke it.
623
+ :::
445
624
 
446
625
  @return DataStoreWriter
447
626
  ]=]
448
627
  function DataStoreStage:GetNewWriter()
449
- local writer = DataStoreWriter.new()
450
- if self._dataToSave then
451
- writer:SetRawData(self._dataToSave)
628
+ self:_checkIntegrity()
629
+
630
+ local writer = DataStoreWriter.new(self:GetFullPath())
631
+
632
+ local fullBaseDataSnapshot = self:_createFullBaseDataSnapshot()
633
+
634
+ if self._saveDataSnapshot ~= nil then
635
+ writer:SetSaveDataSnapshot(self._saveDataSnapshot)
452
636
  end
453
637
 
454
- for name, store in pairs(self._stores) do
638
+ for key, store in pairs(self._stores) do
455
639
  if not store.Destroy then
456
- warn(("[DataStoreStage] - Substore %q destroyed"):format(name))
640
+ warn(("[DataStoreStage] - Substore %q destroyed"):format(key))
457
641
  continue
458
642
  end
459
643
 
460
644
  if store:HasWritableData() then
461
- writer:AddWriter(name, store:GetNewWriter())
645
+ writer:AddSubWriter(key, store:GetNewWriter())
462
646
  end
463
647
  end
464
648
 
649
+ writer:SetFullBaseDataSnapshot(fullBaseDataSnapshot)
650
+
465
651
  return writer
466
652
  end
467
653
 
654
+ --[=[
655
+ Invokes all saving callbacks
656
+
657
+ :::tip
658
+ This is automatically invoked before saving and is public so [DataStore] can invoke it.
659
+ :::
660
+
661
+ @return Promise
662
+ ]=]
663
+ function DataStoreStage:PromiseInvokeSavingCallbacks()
664
+ if not next(self._savingCallbacks) then
665
+ return Promise.resolved()
666
+ end
667
+
668
+ local removingPromises = {}
669
+ for _, func in pairs(self._savingCallbacks) do
670
+ local result = func()
671
+ if Promise.isPromise(result) then
672
+ table.insert(removingPromises, result)
673
+ end
674
+ end
675
+
676
+ for _, substore in pairs(self._stores) do
677
+ local promise = substore:PromiseInvokeSavingCallbacks()
678
+ if promise then
679
+ table.insert(removingPromises, promise)
680
+ end
681
+ end
682
+
683
+ return PromiseUtils.all(removingPromises)
684
+ end
685
+
686
+ function DataStoreStage:_createFullBaseDataSnapshot()
687
+ if self._baseDataSnapshot == DataStoreDeleteToken then
688
+ error("BadDataSnapshot cannot be a delete token")
689
+ elseif type(self._baseDataSnapshot) == "table" or self._baseDataSnapshot == nil then
690
+ local newSnapshot
691
+ if type(self._baseDataSnapshot) == "table" then
692
+ newSnapshot = table.clone(self._baseDataSnapshot)
693
+ else
694
+ newSnapshot = {}
695
+ end
696
+
697
+ for key, store in pairs(self._stores) do
698
+ if not store.Destroy then
699
+ warn(("[DataStoreStage] - Substore %q destroyed"):format(key))
700
+ continue
701
+ end
702
+
703
+ if not store:HasWritableData() then
704
+ newSnapshot[key] = store:_createFullBaseDataSnapshot()
705
+ end
706
+ end
707
+
708
+ if DataStoreSnapshotUtils.isEmptySnapshot(newSnapshot) then
709
+ return nil
710
+ else
711
+ return table.freeze(newSnapshot)
712
+ end
713
+ else
714
+ return self._baseDataSnapshot
715
+ end
716
+ end
717
+
718
+ function DataStoreStage:_updateStoresAndComputeBaseDataSnapshotFromDiffSnapshot(diffSnapshot)
719
+ if diffSnapshot == DataStoreDeleteToken then
720
+ return nil
721
+ elseif type(diffSnapshot) == "table" then
722
+ local newBaseDataSnapshot
723
+ if type(self._baseDataSnapshot) == "table" then
724
+ newBaseDataSnapshot = table.clone(self._baseDataSnapshot)
725
+ else
726
+ newBaseDataSnapshot = {}
727
+ end
728
+
729
+ -- Merge all of our newly downloaded data here into our base layer.
730
+ for key, value in pairs(diffSnapshot) do
731
+ newBaseDataSnapshot[key] = self:_updateStoresAndComputeBaseDataSnapshotValueFromDiffSnapshot(key, value)
732
+ end
733
+
734
+ return table.freeze(newBaseDataSnapshot)
735
+ elseif diffSnapshot == nil then
736
+ return self._baseDataSnapshot
737
+ else
738
+ return diffSnapshot
739
+ end
740
+ end
741
+
742
+ function DataStoreStage:_updateStoresAndComputeBaseDataSnapshotValueFromDiffSnapshot(key, value)
743
+ assert(type(key) == "string" or type(key) == "number", "Bad key")
744
+
745
+ if self._stores[key] then
746
+ self._stores[key]:MergeDiffSnapshot(value)
747
+ return nil
748
+ elseif value == DataStoreDeleteToken then
749
+ return nil
750
+ elseif type(value) == "table" and type(self._baseDataSnapshot) == "table" and type(self._baseDataSnapshot[key]) == "table" then
751
+ return self:_recurseMergeTable(self._baseDataSnapshot[key], value)
752
+ else
753
+ return value
754
+ end
755
+ end
756
+
757
+ function DataStoreStage:_recurseMergeTable(original, incoming)
758
+ if incoming == DataStoreDeleteToken then
759
+ return nil
760
+ elseif type(incoming) == "table" and type(original) == "table" then
761
+ -- Merge
762
+ local newSnapshot = table.clone(original)
763
+
764
+ -- Overwerite with merged values...
765
+ for key, value in pairs(incoming) do
766
+ newSnapshot[key] = self:_recurseMergeTable(original[key], value)
767
+ end
768
+
769
+ return table.freeze(newSnapshot)
770
+ else
771
+ return incoming
772
+ end
773
+ end
774
+
775
+ function DataStoreStage:_updateViewSnapshot()
776
+ self:_checkIntegrity()
777
+
778
+ local newViewSnapshot = self:_computeNewViewSnapshot()
779
+
780
+ if not Table.deepEquivalent(self._viewSnapshot, newViewSnapshot) then
781
+ local previousViewSnapshot = self._viewSnapshot
782
+ self._viewSnapshot = newViewSnapshot
783
+
784
+ -- Fire off changed keys
785
+ local changedKeys = self:_computeChangedKeys(previousViewSnapshot, newViewSnapshot)
786
+ if next(changedKeys) ~= nil then
787
+ if type(newViewSnapshot) == "table" then
788
+ for key, _ in pairs(changedKeys) do
789
+ self._keySubscriptions:Fire(key, newViewSnapshot[key])
790
+ end
791
+ else
792
+ for key, _ in pairs(changedKeys) do
793
+ self._keySubscriptions:Fire(key, nil)
794
+ end
795
+ end
796
+ end
797
+
798
+ self.Changed:Fire(self._viewSnapshot)
799
+ end
800
+
801
+ self:_checkIntegrity()
802
+ end
803
+
804
+ function DataStoreStage:_computeChangedKeys(previousViewSnapshot, newViewSnapshot)
805
+ -- Detect keys that changed
806
+ if type(previousViewSnapshot) == "table" and type(newViewSnapshot) == "table" then
807
+ local changedKeys = {}
808
+
809
+ local keys = Set.union(Set.fromKeys(previousViewSnapshot), Set.fromKeys(newViewSnapshot))
810
+ for key, _ in pairs(keys) do
811
+ if not Table.deepEquivalent(previousViewSnapshot[key], newViewSnapshot[key]) then
812
+ changedKeys[key] = true
813
+ end
814
+ end
815
+
816
+ return changedKeys
817
+ elseif type(newViewSnapshot) == "table" then
818
+ -- Swap to table, all keys change
819
+ return Set.fromKeys(newViewSnapshot)
820
+ elseif type(previousViewSnapshot) == "table" then
821
+ -- Swap from table, all keys change
822
+ return Set.fromKeys(previousViewSnapshot)
823
+ else
824
+ return {}
825
+ end
826
+ end
827
+
828
+ function DataStoreStage:_updateViewSnapshotAtKey(key)
829
+ assert(type(key) == "string" or type(key) == "number", "Bad key")
830
+
831
+ if type(self._viewSnapshot) ~= "table" then
832
+ self:_updateViewSnapshot()
833
+ return
834
+ end
835
+
836
+ local newValue = self:_computeViewValueForKey(key)
837
+ if self._viewSnapshot[key] == newValue then
838
+ return
839
+ end
840
+
841
+ local newSnapshot = table.clone(self._viewSnapshot)
842
+ newSnapshot[key] = newValue
843
+
844
+
845
+ self._viewSnapshot = table.freeze(newSnapshot)
846
+ self._keySubscriptions:Fire(key, newValue)
847
+ self.Changed:Fire(self._viewSnapshot)
848
+
849
+ self:_checkIntegrity()
850
+ end
851
+
852
+ function DataStoreStage:_computeNewViewSnapshot()
853
+ -- This prioritizes save data first, then stores, then base data
854
+
855
+ if self._saveDataSnapshot == DataStoreDeleteToken then
856
+ return nil
857
+ elseif self._saveDataSnapshot == nil or type(self._saveDataSnapshot) == "table" then
858
+ -- Compute a new view
859
+
860
+ -- Start with base data
861
+ local newView
862
+ if type(self._baseDataSnapshot) == "table" then
863
+ newView = table.clone(self._baseDataSnapshot)
864
+ else
865
+ newView = {}
866
+ end
867
+
868
+ -- Add in stores
869
+ for key, store in pairs(self._stores) do
870
+ newView[key] = store._viewSnapshot
871
+ end
872
+
873
+ -- Then finally save data
874
+ if type(self._saveDataSnapshot) == "table" then
875
+ for key, value in pairs(self._saveDataSnapshot) do
876
+ if value == DataStoreDeleteToken then
877
+ newView[key] = nil
878
+ else
879
+ newView[key] = value
880
+ end
881
+ end
882
+ end
883
+
884
+ if next(newView) == nil and not (type(self._baseDataSnapshot) == "table" or type(self._saveDataSnapshot) == "table") then
885
+ -- We haev no reason to be a table, make sure we return nil
886
+ return nil
887
+ end
888
+
889
+ return table.freeze(newView)
890
+ else
891
+ assert(self._saveDataSnapshot ~= nil, "Bad _saveDataSnapshot")
892
+ assert(type(self._saveDataSnapshot) ~= "table", "Bad self._saveDataSnapshot")
893
+
894
+ -- If save data isn't nil or a table then we are to return the save table
895
+ return self._saveDataSnapshot
896
+ end
897
+ end
898
+
899
+ function DataStoreStage:_computeViewValueForKey(key)
900
+ -- This prioritizes save data first, then stores, then base data
901
+
902
+ if self._saveDataSnapshot == DataStoreDeleteToken then
903
+ return nil
904
+ elseif self._saveDataSnapshot == nil or type(self._saveDataSnapshot) == "table" then
905
+ if type(self._saveDataSnapshot) == "table" then
906
+ if self._saveDataSnapshot[key] ~= nil then
907
+ local value = self._saveDataSnapshot[key]
908
+ if value == DataStoreDeleteToken then
909
+ return nil
910
+ else
911
+ return value
912
+ end
913
+ end
914
+ end
915
+
916
+ if self._stores[key] then
917
+ local value = self._stores[key]._viewSnapshot
918
+ if value == DataStoreDeleteToken then
919
+ return nil
920
+ else
921
+ return value
922
+ end
923
+ end
924
+
925
+ if type(self._baseDataSnapshot) == "table" then
926
+ if self._baseDataSnapshot[key] ~= nil then
927
+ return self._baseDataSnapshot[key]
928
+ end
929
+ end
930
+
931
+ return nil
932
+ else
933
+ -- If save data isn't nil or a table then we are to return nil.
934
+ return nil
935
+ end
936
+ end
937
+
468
938
  -- Stores the data for overwrite.
469
- function DataStoreStage:_doStore(name, value)
470
- assert(type(name) == "string" or type(name) == "number", "Bad name")
939
+ function DataStoreStage:_storeAtKey(key, value)
940
+ assert(type(key) == "string" or type(key) == "number", "Bad key")
471
941
  assert(value ~= nil, "Bad value")
472
942
 
473
- local newValue
474
- if value == DataStoreDeleteToken then
475
- newValue = DataStoreDeleteToken
476
- elseif type(value) == "table" then
477
- newValue = Table.deepCopy(value)
943
+ local deepClonedSaveValue
944
+ if type(value) == "table" then
945
+ deepClonedSaveValue = table.freeze(Table.deepCopy(value))
478
946
  else
479
- newValue = value
947
+ deepClonedSaveValue = value
480
948
  end
481
949
 
482
- if not self._dataToSave then
483
- self._dataToSave = {}
950
+ if self._stores[key] then
951
+ self._stores[key]:Overwrite(value)
952
+ return
484
953
  end
485
954
 
486
- self._dataToSave[name] = newValue
487
- if self._topLevelStoreSignal then
488
- self._topLevelStoreSignal:Fire()
955
+ local swappedSaveSnapshotType = false
956
+ local newSnapshot
957
+
958
+ if type(self._saveDataSnapshot) == "table" then
959
+ newSnapshot = table.clone(self._saveDataSnapshot)
960
+ else
961
+ swappedSaveSnapshotType = true
962
+ newSnapshot = {}
489
963
  end
490
964
 
491
- if newValue == DataStoreDeleteToken then
492
- self._subsTable:Fire(name, nil)
965
+ newSnapshot[key] = deepClonedSaveValue
966
+
967
+ self._saveDataSnapshot = table.freeze(newSnapshot)
968
+
969
+ self.DataStored:Fire()
970
+
971
+ if swappedSaveSnapshotType then
972
+ self:_updateViewSnapshot()
493
973
  else
494
- self._subsTable:Fire(name, newValue)
974
+ self:_updateViewSnapshotAtKey(key)
975
+ end
976
+ self:_checkIntegrity()
977
+ end
978
+
979
+ function DataStoreStage:_checkIntegrity()
980
+ if not SLOW_INTEGRITY_CHECK_ENABLED then
981
+ return
982
+ end
983
+
984
+ assert(self._baseDataSnapshot ~= DataStoreDeleteToken, "BaseDataSnapshot should not be DataStoreDeleteToken")
985
+ assert(self._viewSnapshot ~= DataStoreDeleteToken, "ViewSnapshot should not be DataStoreDeleteToken")
986
+
987
+ if type(self._baseDataSnapshot) == "table" then
988
+ assert(table.isfrozen(self._baseDataSnapshot), "Base snapshot should be frozen")
989
+ end
990
+
991
+ if type(self._saveDataSnapshot) == "table" then
992
+ assert(table.isfrozen(self._saveDataSnapshot), "Save snapshot should be frozen")
993
+ end
994
+
995
+ if type(self._viewSnapshot) == "table" then
996
+ assert(table.isfrozen(self._viewSnapshot), "View snapshot should be frozen")
997
+ end
998
+
999
+ for key, _ in pairs(self._stores) do
1000
+ if type(self._baseDataSnapshot) == "table" and self._baseDataSnapshot[key] ~= nil then
1001
+ error(string.format("[DataStoreStage] - Duplicate baseData at key %q", key))
1002
+ end
1003
+
1004
+ if type(self._saveDataSnapshot) == "table" and self._saveDataSnapshot[key] ~= nil then
1005
+ error(string.format("[DataStoreStage] - Duplicate saveData at key %q", key))
1006
+ end
1007
+ end
1008
+
1009
+ if type(self._viewSnapshot) == "table" then
1010
+ for key, value in pairs(self._viewSnapshot) do
1011
+ assert(type(key) == "string" or type(key) == "number", "Bad key")
1012
+ if value == DataStoreDeleteToken then
1013
+ error(string.format("[DataStoreStage] - View at key %q is delete token", key))
1014
+ end
1015
+ end
495
1016
  end
496
1017
  end
497
1018