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