@quenty/datastore 13.39.1 → 13.41.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.
- package/CHANGELOG.md +8 -0
- package/package.json +9 -9
- package/src/Server/DataStore.HookErrors.spec.lua +92 -0
- package/src/Server/DataStore.LoadErrors.spec.lua +133 -0
- package/src/Server/DataStore.Reentrance.spec.lua +119 -0
- package/src/Server/DataStore.SessionLock.spec.lua +478 -0
- package/src/Server/DataStore.SessionMessaging.spec.lua +71 -0
- package/src/Server/DataStore.TwoServerLock.spec.lua +190 -0
- package/src/Server/DataStore.lua +89 -18
- package/src/Server/DataStore.spec.lua +237 -0
- package/src/Server/DataStoreLockHelper.spec.lua +239 -0
- package/src/Server/DataStoreMessageHelper.spec.lua +134 -0
- package/src/Server/DataStoreNonRetryableLoadError.lua +61 -0
- package/src/Server/DataStoreTestUtils.lua +206 -0
- package/src/Server/GameDataStoreService.lua +26 -0
- package/src/Server/GameDataStoreService.spec.lua +215 -0
- package/src/Server/Mocks/DataStoreMock.lua +405 -0
- package/src/Server/Mocks/DataStoreMock.spec.lua +384 -0
- package/src/Server/Modules/DataStoreSnapshotUtils.spec.lua +91 -0
- package/src/Server/Modules/DataStoreStage.lua +8 -2
- package/src/Server/Modules/DataStoreStage.spec.lua +539 -0
- package/src/Server/Modules/DataStoreWriter.spec.lua +399 -0
- package/src/Server/PlayerDataStoreManager.RemovalCallbacks.spec.lua +99 -0
- package/src/Server/PlayerDataStoreManager.lua +36 -4
- package/src/Server/PlayerDataStoreManager.spec.lua +202 -0
- package/src/Server/PlayerDataStoreService.lua +18 -0
- package/src/Server/PlayerDataStoreService.spec.lua +218 -0
- package/src/Server/PrivateServerDataStoreService.lua +23 -1
- package/src/Server/PrivateServerDataStoreService.spec.lua +242 -0
- package/src/Server/Utility/DataStorePromises.lua +18 -5
- package/src/Server/Utility/DataStorePromises.spec.lua +363 -0
- package/src/jest.config.lua +3 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
--!nonstrict
|
|
2
|
+
--[[
|
|
3
|
+
Characterization coverage for the DataStoreStage staging layer, exercised through a real root
|
|
4
|
+
(`DataStore.new(DataStoreMock.new(), key)`) since a bare stage has no load parent. Stores here are
|
|
5
|
+
non-session-locking, so staging is deterministic.
|
|
6
|
+
|
|
7
|
+
@class DataStoreStage.spec.lua
|
|
8
|
+
]]
|
|
9
|
+
local require = require(script.Parent.loader).load(script)
|
|
10
|
+
|
|
11
|
+
local DataStore = require("DataStore")
|
|
12
|
+
local DataStoreMock = require("DataStoreMock")
|
|
13
|
+
local Jest = require("Jest")
|
|
14
|
+
local Promise = require("Promise")
|
|
15
|
+
local PromiseTestUtils = require("PromiseTestUtils")
|
|
16
|
+
local Rx = require("Rx")
|
|
17
|
+
local ValueObject = require("ValueObject")
|
|
18
|
+
|
|
19
|
+
local describe = Jest.Globals.describe
|
|
20
|
+
local expect = Jest.Globals.expect
|
|
21
|
+
local it = Jest.Globals.it
|
|
22
|
+
|
|
23
|
+
describe("DataStoreStage staging (through a DataStore root)", function()
|
|
24
|
+
describe("Store / Load", function()
|
|
25
|
+
it("should Load back a value that was Stored", function()
|
|
26
|
+
local dataStore = DataStore.new(DataStoreMock.new(), "player_1")
|
|
27
|
+
dataStore:Store("coins", 25)
|
|
28
|
+
|
|
29
|
+
local promise = dataStore:Load("coins")
|
|
30
|
+
if not PromiseTestUtils.awaitSettled(promise) then
|
|
31
|
+
expect("hung").toEqual("settled")
|
|
32
|
+
return
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
local ok, value = promise:Yield()
|
|
36
|
+
expect(ok).toEqual(true)
|
|
37
|
+
expect(value).toEqual(25)
|
|
38
|
+
|
|
39
|
+
dataStore:Destroy()
|
|
40
|
+
end)
|
|
41
|
+
|
|
42
|
+
it("should Load a deep-equal table that was Stored", function()
|
|
43
|
+
local dataStore = DataStore.new(DataStoreMock.new(), "player_1")
|
|
44
|
+
dataStore:Store("profile", { level = 3, name = "Egg" })
|
|
45
|
+
|
|
46
|
+
local promise = dataStore:Load("profile")
|
|
47
|
+
if not PromiseTestUtils.awaitSettled(promise) then
|
|
48
|
+
expect("hung").toEqual("settled")
|
|
49
|
+
return
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
local ok, value = promise:Yield()
|
|
53
|
+
expect(ok).toEqual(true)
|
|
54
|
+
expect(value).toEqual({ level = 3, name = "Egg" })
|
|
55
|
+
|
|
56
|
+
dataStore:Destroy()
|
|
57
|
+
end)
|
|
58
|
+
|
|
59
|
+
it("should return the default value for a missing key", function()
|
|
60
|
+
local dataStore = DataStore.new(DataStoreMock.new(), "player_1")
|
|
61
|
+
|
|
62
|
+
local promise = dataStore:Load("missing", "fallback")
|
|
63
|
+
if not PromiseTestUtils.awaitSettled(promise) then
|
|
64
|
+
expect("hung").toEqual("settled")
|
|
65
|
+
return
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
expect((promise:Wait())).toEqual("fallback")
|
|
69
|
+
|
|
70
|
+
dataStore:Destroy()
|
|
71
|
+
end)
|
|
72
|
+
end)
|
|
73
|
+
|
|
74
|
+
describe("LoadAll", function()
|
|
75
|
+
it("should return the whole staged view", function()
|
|
76
|
+
local dataStore = DataStore.new(DataStoreMock.new(), "player_1")
|
|
77
|
+
dataStore:Store("coins", 5)
|
|
78
|
+
dataStore:Store("gems", 10)
|
|
79
|
+
|
|
80
|
+
local promise = dataStore:LoadAll()
|
|
81
|
+
if not PromiseTestUtils.awaitSettled(promise) then
|
|
82
|
+
expect("hung").toEqual("settled")
|
|
83
|
+
return
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
local ok, all = promise:Yield()
|
|
87
|
+
expect(ok).toEqual(true)
|
|
88
|
+
expect(all).toEqual({ coins = 5, gems = 10 })
|
|
89
|
+
|
|
90
|
+
dataStore:Destroy()
|
|
91
|
+
end)
|
|
92
|
+
|
|
93
|
+
it("should return the default when the stage is empty", function()
|
|
94
|
+
local dataStore = DataStore.new(DataStoreMock.new(), "player_1")
|
|
95
|
+
|
|
96
|
+
local promise = dataStore:LoadAll("empty")
|
|
97
|
+
if not PromiseTestUtils.awaitSettled(promise) then
|
|
98
|
+
expect("hung").toEqual("settled")
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
expect((promise:Wait())).toEqual("empty")
|
|
103
|
+
|
|
104
|
+
dataStore:Destroy()
|
|
105
|
+
end)
|
|
106
|
+
end)
|
|
107
|
+
|
|
108
|
+
describe("GetSubStore", function()
|
|
109
|
+
it("should return the same substore instance for the same key", function()
|
|
110
|
+
local dataStore = DataStore.new(DataStoreMock.new(), "player_1")
|
|
111
|
+
|
|
112
|
+
local first = dataStore:GetSubStore("inv")
|
|
113
|
+
local second = dataStore:GetSubStore("inv")
|
|
114
|
+
expect(first).toEqual(second)
|
|
115
|
+
|
|
116
|
+
dataStore:Destroy()
|
|
117
|
+
end)
|
|
118
|
+
|
|
119
|
+
it("should surface a substore value under its key in the parent LoadAll", function()
|
|
120
|
+
local dataStore = DataStore.new(DataStoreMock.new(), "player_1")
|
|
121
|
+
dataStore:GetSubStore("inv"):Store("sword", true)
|
|
122
|
+
|
|
123
|
+
local promise = dataStore:LoadAll()
|
|
124
|
+
if not PromiseTestUtils.awaitSettled(promise) then
|
|
125
|
+
expect("hung").toEqual("settled")
|
|
126
|
+
return
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
local ok, all = promise:Yield()
|
|
130
|
+
expect(ok).toEqual(true)
|
|
131
|
+
expect(all).toEqual({ inv = { sword = true } })
|
|
132
|
+
|
|
133
|
+
dataStore:Destroy()
|
|
134
|
+
end)
|
|
135
|
+
|
|
136
|
+
it("should support deep nesting (substore of a substore)", function()
|
|
137
|
+
local dataStore = DataStore.new(DataStoreMock.new(), "player_1")
|
|
138
|
+
dataStore:GetSubStore("a"):GetSubStore("b"):Store("c", 1)
|
|
139
|
+
|
|
140
|
+
local promise = dataStore:LoadAll()
|
|
141
|
+
if not PromiseTestUtils.awaitSettled(promise) then
|
|
142
|
+
expect("hung").toEqual("settled")
|
|
143
|
+
return
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
local ok, all = promise:Yield()
|
|
147
|
+
expect(ok).toEqual(true)
|
|
148
|
+
expect(all).toEqual({ a = { b = { c = 1 } } })
|
|
149
|
+
|
|
150
|
+
dataStore:Destroy()
|
|
151
|
+
end)
|
|
152
|
+
end)
|
|
153
|
+
|
|
154
|
+
describe("View priority", function()
|
|
155
|
+
it("should prioritize staged save data over loaded base data", function()
|
|
156
|
+
local mock = DataStoreMock.new()
|
|
157
|
+
-- Seed base data BEFORE constructing the reader so it loads through getAsync.
|
|
158
|
+
mock:SetRaw("player_1", { coins = 1, gems = 2 })
|
|
159
|
+
|
|
160
|
+
local dataStore = DataStore.new(mock, "player_1")
|
|
161
|
+
|
|
162
|
+
-- Base data loads first
|
|
163
|
+
local basePromise = dataStore:Load("coins")
|
|
164
|
+
if not PromiseTestUtils.awaitSettled(basePromise) then
|
|
165
|
+
expect("hung").toEqual("settled")
|
|
166
|
+
return
|
|
167
|
+
end
|
|
168
|
+
expect((basePromise:Wait())).toEqual(1)
|
|
169
|
+
|
|
170
|
+
-- Staged value overrides base; untouched base key stays visible
|
|
171
|
+
dataStore:Store("coins", 999)
|
|
172
|
+
|
|
173
|
+
local promise = dataStore:LoadAll()
|
|
174
|
+
if not PromiseTestUtils.awaitSettled(promise) then
|
|
175
|
+
expect("hung").toEqual("settled")
|
|
176
|
+
return
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
local ok, all = promise:Yield()
|
|
180
|
+
expect(ok).toEqual(true)
|
|
181
|
+
expect(all).toEqual({ coins = 999, gems = 2 })
|
|
182
|
+
|
|
183
|
+
dataStore:Destroy()
|
|
184
|
+
end)
|
|
185
|
+
end)
|
|
186
|
+
|
|
187
|
+
describe("Overwrite / OverwriteMerge / Wipe", function()
|
|
188
|
+
it("should replace the whole staged view with Overwrite", function()
|
|
189
|
+
local dataStore = DataStore.new(DataStoreMock.new(), "player_1")
|
|
190
|
+
dataStore:Store("a", 1)
|
|
191
|
+
dataStore:Store("b", 2)
|
|
192
|
+
|
|
193
|
+
dataStore:Overwrite({ c = 3 })
|
|
194
|
+
|
|
195
|
+
local promise = dataStore:LoadAll()
|
|
196
|
+
if not PromiseTestUtils.awaitSettled(promise) then
|
|
197
|
+
expect("hung").toEqual("settled")
|
|
198
|
+
return
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
local ok, all = promise:Yield()
|
|
202
|
+
expect(ok).toEqual(true)
|
|
203
|
+
expect(all).toEqual({ c = 3 })
|
|
204
|
+
|
|
205
|
+
dataStore:Destroy()
|
|
206
|
+
end)
|
|
207
|
+
|
|
208
|
+
it("should clear the staged view with Overwrite(nil)", function()
|
|
209
|
+
local dataStore = DataStore.new(DataStoreMock.new(), "player_1")
|
|
210
|
+
dataStore:Store("a", 1)
|
|
211
|
+
|
|
212
|
+
dataStore:Overwrite(nil)
|
|
213
|
+
|
|
214
|
+
local promise = dataStore:LoadAll("cleared")
|
|
215
|
+
if not PromiseTestUtils.awaitSettled(promise) then
|
|
216
|
+
expect("hung").toEqual("settled")
|
|
217
|
+
return
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
expect((promise:Wait())).toEqual("cleared")
|
|
221
|
+
|
|
222
|
+
dataStore:Destroy()
|
|
223
|
+
end)
|
|
224
|
+
|
|
225
|
+
it("should clear even loaded base data with Wipe", function()
|
|
226
|
+
local mock = DataStoreMock.new()
|
|
227
|
+
mock:SetRaw("player_1", { coins = 5 })
|
|
228
|
+
|
|
229
|
+
local dataStore = DataStore.new(mock, "player_1")
|
|
230
|
+
|
|
231
|
+
-- Force base to load first
|
|
232
|
+
local basePromise = dataStore:Load("coins")
|
|
233
|
+
if not PromiseTestUtils.awaitSettled(basePromise) then
|
|
234
|
+
expect("hung").toEqual("settled")
|
|
235
|
+
return
|
|
236
|
+
end
|
|
237
|
+
expect((basePromise:Wait())).toEqual(5)
|
|
238
|
+
|
|
239
|
+
dataStore:Wipe()
|
|
240
|
+
|
|
241
|
+
local promise = dataStore:LoadAll("wiped")
|
|
242
|
+
if not PromiseTestUtils.awaitSettled(promise) then
|
|
243
|
+
expect("hung").toEqual("settled")
|
|
244
|
+
return
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
expect((promise:Wait())).toEqual("wiped")
|
|
248
|
+
|
|
249
|
+
dataStore:Destroy()
|
|
250
|
+
end)
|
|
251
|
+
|
|
252
|
+
it("should merge without wiping missing keys via OverwriteMerge", function()
|
|
253
|
+
local dataStore = DataStore.new(DataStoreMock.new(), "player_1")
|
|
254
|
+
dataStore:Store("a", 1)
|
|
255
|
+
dataStore:Store("b", 2)
|
|
256
|
+
|
|
257
|
+
dataStore:OverwriteMerge({ b = 3, c = 4 })
|
|
258
|
+
|
|
259
|
+
local promise = dataStore:LoadAll()
|
|
260
|
+
if not PromiseTestUtils.awaitSettled(promise) then
|
|
261
|
+
expect("hung").toEqual("settled")
|
|
262
|
+
return
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
local ok, all = promise:Yield()
|
|
266
|
+
expect(ok).toEqual(true)
|
|
267
|
+
expect(all).toEqual({ a = 1, b = 3, c = 4 })
|
|
268
|
+
|
|
269
|
+
dataStore:Destroy()
|
|
270
|
+
end)
|
|
271
|
+
end)
|
|
272
|
+
|
|
273
|
+
describe("Delete", function()
|
|
274
|
+
it("should remove a key so it no longer loads", function()
|
|
275
|
+
local dataStore = DataStore.new(DataStoreMock.new(), "player_1")
|
|
276
|
+
dataStore:Store("a", 1)
|
|
277
|
+
dataStore:Store("b", 2)
|
|
278
|
+
|
|
279
|
+
dataStore:Delete("a")
|
|
280
|
+
|
|
281
|
+
local promise = dataStore:LoadAll()
|
|
282
|
+
if not PromiseTestUtils.awaitSettled(promise) then
|
|
283
|
+
expect("hung").toEqual("settled")
|
|
284
|
+
return
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
local ok, all = promise:Yield()
|
|
288
|
+
expect(ok).toEqual(true)
|
|
289
|
+
expect(all).toEqual({ b = 2 })
|
|
290
|
+
|
|
291
|
+
dataStore:Destroy()
|
|
292
|
+
end)
|
|
293
|
+
end)
|
|
294
|
+
|
|
295
|
+
describe("Observe", function()
|
|
296
|
+
it("should emit the initial value for a key", function()
|
|
297
|
+
local mock = DataStoreMock.new()
|
|
298
|
+
mock:SetRaw("player_1", { coins = 7 })
|
|
299
|
+
|
|
300
|
+
local dataStore = DataStore.new(mock, "player_1")
|
|
301
|
+
|
|
302
|
+
local promise = Rx.toPromise(dataStore:Observe("coins", 0))
|
|
303
|
+
if not PromiseTestUtils.awaitSettled(promise) then
|
|
304
|
+
expect("hung").toEqual("settled")
|
|
305
|
+
return
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
local ok, value = promise:Yield()
|
|
309
|
+
expect(ok).toEqual(true)
|
|
310
|
+
expect(value).toEqual(7)
|
|
311
|
+
|
|
312
|
+
dataStore:Destroy()
|
|
313
|
+
end)
|
|
314
|
+
|
|
315
|
+
it("should emit the default for a missing key", function()
|
|
316
|
+
local dataStore = DataStore.new(DataStoreMock.new(), "player_1")
|
|
317
|
+
|
|
318
|
+
local promise = Rx.toPromise(dataStore:Observe("coins", 0))
|
|
319
|
+
if not PromiseTestUtils.awaitSettled(promise) then
|
|
320
|
+
expect("hung").toEqual("settled")
|
|
321
|
+
return
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
expect((promise:Wait())).toEqual(0)
|
|
325
|
+
|
|
326
|
+
dataStore:Destroy()
|
|
327
|
+
end)
|
|
328
|
+
|
|
329
|
+
it("should fire the observer with the new value after a Store on a key", function()
|
|
330
|
+
local dataStore = DataStore.new(DataStoreMock.new(), "player_1")
|
|
331
|
+
|
|
332
|
+
local captured
|
|
333
|
+
local sub = dataStore:Observe("coins", 0):Subscribe(function(value)
|
|
334
|
+
captured = value
|
|
335
|
+
end)
|
|
336
|
+
|
|
337
|
+
dataStore:Store("coins", 42)
|
|
338
|
+
|
|
339
|
+
expect(PromiseTestUtils.awaitValue(function()
|
|
340
|
+
return captured == 42
|
|
341
|
+
end)).toEqual(true)
|
|
342
|
+
|
|
343
|
+
sub:Destroy()
|
|
344
|
+
dataStore:Destroy()
|
|
345
|
+
end)
|
|
346
|
+
|
|
347
|
+
it("should fire the whole-view observer with the new snapshot after a Store", function()
|
|
348
|
+
local dataStore = DataStore.new(DataStoreMock.new(), "player_1")
|
|
349
|
+
|
|
350
|
+
-- The initial emission of an empty store is nil, so count emissions (not value) to
|
|
351
|
+
-- confirm the Changed connection is established before we Store.
|
|
352
|
+
local emissions = 0
|
|
353
|
+
local captured
|
|
354
|
+
local sub = dataStore:Observe():Subscribe(function(snapshot)
|
|
355
|
+
emissions += 1
|
|
356
|
+
captured = snapshot
|
|
357
|
+
end)
|
|
358
|
+
|
|
359
|
+
expect(PromiseTestUtils.awaitValue(function()
|
|
360
|
+
return emissions >= 1
|
|
361
|
+
end)).toEqual(true)
|
|
362
|
+
|
|
363
|
+
dataStore:Store("coins", 1)
|
|
364
|
+
|
|
365
|
+
expect(PromiseTestUtils.awaitValue(function()
|
|
366
|
+
return type(captured) == "table" and captured.coins == 1
|
|
367
|
+
end)).toEqual(true)
|
|
368
|
+
|
|
369
|
+
sub:Destroy()
|
|
370
|
+
dataStore:Destroy()
|
|
371
|
+
end)
|
|
372
|
+
end)
|
|
373
|
+
|
|
374
|
+
describe("PromiseKeyList / PromiseKeySet", function()
|
|
375
|
+
it("should report the set of staged keys", function()
|
|
376
|
+
local dataStore = DataStore.new(DataStoreMock.new(), "player_1")
|
|
377
|
+
dataStore:Store("a", 1)
|
|
378
|
+
dataStore:Store("b", 2)
|
|
379
|
+
|
|
380
|
+
local promise = dataStore:PromiseKeySet()
|
|
381
|
+
if not PromiseTestUtils.awaitSettled(promise) then
|
|
382
|
+
expect("hung").toEqual("settled")
|
|
383
|
+
return
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
local ok, keys = promise:Yield()
|
|
387
|
+
expect(ok).toEqual(true)
|
|
388
|
+
expect(keys).toEqual({ a = true, b = true })
|
|
389
|
+
|
|
390
|
+
dataStore:Destroy()
|
|
391
|
+
end)
|
|
392
|
+
|
|
393
|
+
it("should report the list of staged keys", function()
|
|
394
|
+
local dataStore = DataStore.new(DataStoreMock.new(), "player_1")
|
|
395
|
+
dataStore:Store("a", 1)
|
|
396
|
+
dataStore:Store("b", 2)
|
|
397
|
+
|
|
398
|
+
local promise = dataStore:PromiseKeyList()
|
|
399
|
+
if not PromiseTestUtils.awaitSettled(promise) then
|
|
400
|
+
expect("hung").toEqual("settled")
|
|
401
|
+
return
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
local ok, list = promise:Yield()
|
|
405
|
+
expect(ok).toEqual(true)
|
|
406
|
+
|
|
407
|
+
-- Order is not guaranteed; compare as a set
|
|
408
|
+
local asSet = {}
|
|
409
|
+
for _, key in list do
|
|
410
|
+
asSet[key] = true
|
|
411
|
+
end
|
|
412
|
+
expect(asSet).toEqual({ a = true, b = true })
|
|
413
|
+
|
|
414
|
+
dataStore:Destroy()
|
|
415
|
+
end)
|
|
416
|
+
end)
|
|
417
|
+
|
|
418
|
+
describe("StoreOnValueChange", function()
|
|
419
|
+
it("should stage the value whenever the ValueObject changes", function()
|
|
420
|
+
local dataStore = DataStore.new(DataStoreMock.new(), "player_1")
|
|
421
|
+
|
|
422
|
+
-- Ensure the stage is loaded before wiring up the value object
|
|
423
|
+
if not PromiseTestUtils.awaitSettled(dataStore:Load("level")) then
|
|
424
|
+
expect("hung").toEqual("settled")
|
|
425
|
+
return
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
local valueObject = ValueObject.new(0)
|
|
429
|
+
dataStore:StoreOnValueChange("level", valueObject)
|
|
430
|
+
|
|
431
|
+
-- Construction does not stage; a change does
|
|
432
|
+
valueObject.Value = 7
|
|
433
|
+
|
|
434
|
+
local promise = dataStore:LoadAll()
|
|
435
|
+
if not PromiseTestUtils.awaitSettled(promise) then
|
|
436
|
+
expect("hung").toEqual("settled")
|
|
437
|
+
return
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
local ok, all = promise:Yield()
|
|
441
|
+
expect(ok).toEqual(true)
|
|
442
|
+
expect(all.level).toEqual(7)
|
|
443
|
+
|
|
444
|
+
dataStore:Destroy()
|
|
445
|
+
end)
|
|
446
|
+
end)
|
|
447
|
+
|
|
448
|
+
describe("Persistence round-trip", function()
|
|
449
|
+
it("should persist a staged value across a fresh DataStore on the same mock", function()
|
|
450
|
+
local mock = DataStoreMock.new()
|
|
451
|
+
|
|
452
|
+
local writer = DataStore.new(mock, "player_1")
|
|
453
|
+
writer:Store("coins", 5)
|
|
454
|
+
|
|
455
|
+
local savePromise = writer:Save()
|
|
456
|
+
if not PromiseTestUtils.awaitSettled(savePromise) then
|
|
457
|
+
expect("hung").toEqual("settled")
|
|
458
|
+
return
|
|
459
|
+
end
|
|
460
|
+
expect((savePromise:Yield())).toEqual(true)
|
|
461
|
+
|
|
462
|
+
local reader = DataStore.new(mock, "player_1")
|
|
463
|
+
local loadPromise = reader:Load("coins")
|
|
464
|
+
if not PromiseTestUtils.awaitSettled(loadPromise) then
|
|
465
|
+
expect("hung").toEqual("settled")
|
|
466
|
+
return
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
local ok, value = loadPromise:Yield()
|
|
470
|
+
expect(ok).toEqual(true)
|
|
471
|
+
expect(value).toEqual(5)
|
|
472
|
+
|
|
473
|
+
writer:Destroy()
|
|
474
|
+
reader:Destroy()
|
|
475
|
+
end)
|
|
476
|
+
end)
|
|
477
|
+
|
|
478
|
+
describe("Deep-copy isolation", function()
|
|
479
|
+
it("should not reflect mutations to the input table after Store", function()
|
|
480
|
+
local dataStore = DataStore.new(DataStoreMock.new(), "player_1")
|
|
481
|
+
|
|
482
|
+
local input = { count = 1 }
|
|
483
|
+
dataStore:Store("data", input)
|
|
484
|
+
|
|
485
|
+
-- Mutating the caller's table must not change the frozen staged copy
|
|
486
|
+
input.count = 999
|
|
487
|
+
|
|
488
|
+
local promise = dataStore:Load("data")
|
|
489
|
+
if not PromiseTestUtils.awaitSettled(promise) then
|
|
490
|
+
expect("hung").toEqual("settled")
|
|
491
|
+
return
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
local ok, value = promise:Yield()
|
|
495
|
+
expect(ok).toEqual(true)
|
|
496
|
+
expect(value).toEqual({ count = 1 })
|
|
497
|
+
|
|
498
|
+
dataStore:Destroy()
|
|
499
|
+
end)
|
|
500
|
+
end)
|
|
501
|
+
|
|
502
|
+
describe("PromiseInvokeSavingCallbacks with promise-returning callbacks", function()
|
|
503
|
+
it("resolves when a callback's returned promise resolves", function()
|
|
504
|
+
local dataStore = DataStore.new(DataStoreMock.new(), "player_1")
|
|
505
|
+
dataStore:AddSavingCallback(function()
|
|
506
|
+
return Promise.resolved()
|
|
507
|
+
end)
|
|
508
|
+
|
|
509
|
+
local outcome = PromiseTestUtils.awaitOutcome(dataStore:PromiseInvokeSavingCallbacks())
|
|
510
|
+
expect(outcome).toEqual("resolved")
|
|
511
|
+
|
|
512
|
+
dataStore:Destroy()
|
|
513
|
+
end)
|
|
514
|
+
|
|
515
|
+
it("rejects when a callback's returned promise rejects, so the save fails", function()
|
|
516
|
+
local dataStore = DataStore.new(DataStoreMock.new(), "player_1")
|
|
517
|
+
dataStore:AddSavingCallback(function()
|
|
518
|
+
return Promise.rejected("removing callback boom")
|
|
519
|
+
end)
|
|
520
|
+
|
|
521
|
+
local outcome, err = PromiseTestUtils.awaitOutcome(dataStore:PromiseInvokeSavingCallbacks())
|
|
522
|
+
expect(outcome).toEqual("rejected")
|
|
523
|
+
expect(string.find(tostring(err), "removing callback boom", 1, true) ~= nil).toEqual(true)
|
|
524
|
+
|
|
525
|
+
dataStore:Destroy()
|
|
526
|
+
end)
|
|
527
|
+
|
|
528
|
+
it("never settles when a callback's returned promise never resolves, so the save hangs", function()
|
|
529
|
+
local dataStore = DataStore.new(DataStoreMock.new(), "player_1")
|
|
530
|
+
dataStore:AddSavingCallback(function()
|
|
531
|
+
return Promise.new()
|
|
532
|
+
end)
|
|
533
|
+
|
|
534
|
+
expect(PromiseTestUtils.awaitSettled(dataStore:PromiseInvokeSavingCallbacks(), 0.25)).toEqual(false)
|
|
535
|
+
|
|
536
|
+
dataStore:Destroy()
|
|
537
|
+
end)
|
|
538
|
+
end)
|
|
539
|
+
end)
|