@quenty/datastore 13.40.0 → 13.42.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 +10 -0
- package/package.json +12 -12
- 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/Shared/Utility/DataStoreStringUtils.spec.lua +1 -1
- package/src/jest.config.lua +3 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
--!nonstrict
|
|
2
|
+
--[[
|
|
3
|
+
Sanity coverage for the DataStoreMock itself, so tests that rely on it can trust its
|
|
4
|
+
datastore-faithful behavior (deep-copy round-tripping, UpdateAsync transform semantics,
|
|
5
|
+
and failure injection).
|
|
6
|
+
|
|
7
|
+
Note: the mock's GetAsync/UpdateAsync/RemoveAsync return `(value, keyInfo)`; jest-lua's
|
|
8
|
+
`expect` takes exactly one argument, so those results are wrapped in parens to keep only
|
|
9
|
+
the value.
|
|
10
|
+
|
|
11
|
+
@class DataStoreMock.spec.lua
|
|
12
|
+
]]
|
|
13
|
+
local require = require(script.Parent.loader).load(script)
|
|
14
|
+
|
|
15
|
+
local DataStoreMock = require("DataStoreMock")
|
|
16
|
+
local Jest = require("Jest")
|
|
17
|
+
|
|
18
|
+
local describe = Jest.Globals.describe
|
|
19
|
+
local expect = Jest.Globals.expect
|
|
20
|
+
local it = Jest.Globals.it
|
|
21
|
+
|
|
22
|
+
describe("DataStoreMock.isDataStoreMock", function()
|
|
23
|
+
it("should be true for a mock", function()
|
|
24
|
+
expect(DataStoreMock.isDataStoreMock(DataStoreMock.new())).toEqual(true)
|
|
25
|
+
end)
|
|
26
|
+
|
|
27
|
+
it("should be false for a plain table", function()
|
|
28
|
+
expect(DataStoreMock.isDataStoreMock({})).toEqual(false)
|
|
29
|
+
end)
|
|
30
|
+
|
|
31
|
+
it("should be false for a table with a different metatable", function()
|
|
32
|
+
expect(DataStoreMock.isDataStoreMock(setmetatable({}, { __index = {} }))).toEqual(false)
|
|
33
|
+
end)
|
|
34
|
+
|
|
35
|
+
it("should be false for nil and primitives", function()
|
|
36
|
+
expect(DataStoreMock.isDataStoreMock(nil)).toEqual(false)
|
|
37
|
+
expect(DataStoreMock.isDataStoreMock(5)).toEqual(false)
|
|
38
|
+
expect(DataStoreMock.isDataStoreMock("str")).toEqual(false)
|
|
39
|
+
expect(DataStoreMock.isDataStoreMock(true)).toEqual(false)
|
|
40
|
+
end)
|
|
41
|
+
end)
|
|
42
|
+
|
|
43
|
+
describe("DataStoreMock:GetAsync", function()
|
|
44
|
+
it("should return nil for an unset key", function()
|
|
45
|
+
local store = DataStoreMock.new()
|
|
46
|
+
expect((store:GetAsync("missing"))).toEqual(nil)
|
|
47
|
+
end)
|
|
48
|
+
|
|
49
|
+
it("should return the value set via SetAsync", function()
|
|
50
|
+
local store = DataStoreMock.new()
|
|
51
|
+
store:SetAsync("key", 42)
|
|
52
|
+
expect((store:GetAsync("key"))).toEqual(42)
|
|
53
|
+
end)
|
|
54
|
+
|
|
55
|
+
it("should return a keyInfo as its second result", function()
|
|
56
|
+
local store = DataStoreMock.new()
|
|
57
|
+
store:SetAsync("key", 1)
|
|
58
|
+
|
|
59
|
+
local _, keyInfo = store:GetAsync("key")
|
|
60
|
+
expect(keyInfo).never.toBeNil()
|
|
61
|
+
expect(type(keyInfo.GetUserIds)).toEqual("function")
|
|
62
|
+
expect(type(keyInfo.GetMetadata)).toEqual("function")
|
|
63
|
+
end)
|
|
64
|
+
|
|
65
|
+
it("should return a deep copy so mutating the result does not affect storage", function()
|
|
66
|
+
local store = DataStoreMock.new()
|
|
67
|
+
store:SetAsync("key", { coins = 5 })
|
|
68
|
+
|
|
69
|
+
local first = store:GetAsync("key")
|
|
70
|
+
first.coins = 999
|
|
71
|
+
|
|
72
|
+
local second = store:GetAsync("key")
|
|
73
|
+
expect(second.coins).toEqual(5)
|
|
74
|
+
end)
|
|
75
|
+
|
|
76
|
+
it("should return independent copies across nested tables", function()
|
|
77
|
+
local store = DataStoreMock.new()
|
|
78
|
+
store:SetAsync("key", { nested = { list = { 1, 2, 3 } } })
|
|
79
|
+
|
|
80
|
+
local first = store:GetAsync("key")
|
|
81
|
+
table.insert(first.nested.list, 4)
|
|
82
|
+
|
|
83
|
+
local second = store:GetAsync("key")
|
|
84
|
+
expect(#second.nested.list).toEqual(3)
|
|
85
|
+
end)
|
|
86
|
+
end)
|
|
87
|
+
|
|
88
|
+
describe("DataStoreMock:SetAsync", function()
|
|
89
|
+
it("should overwrite an existing value", function()
|
|
90
|
+
local store = DataStoreMock.new()
|
|
91
|
+
store:SetAsync("key", "first")
|
|
92
|
+
store:SetAsync("key", "second")
|
|
93
|
+
expect((store:GetAsync("key"))).toEqual("second")
|
|
94
|
+
end)
|
|
95
|
+
|
|
96
|
+
it("should not alias the stored value to the caller's table", function()
|
|
97
|
+
local store = DataStoreMock.new()
|
|
98
|
+
local value = { coins = 5 }
|
|
99
|
+
store:SetAsync("key", value)
|
|
100
|
+
value.coins = 999
|
|
101
|
+
expect((store:GetAsync("key")).coins).toEqual(5)
|
|
102
|
+
end)
|
|
103
|
+
|
|
104
|
+
it("should record associated userIds", function()
|
|
105
|
+
local store = DataStoreMock.new()
|
|
106
|
+
store:SetAsync("key", 1, { 111, 222 })
|
|
107
|
+
|
|
108
|
+
local _, keyInfo = store:GetAsync("key")
|
|
109
|
+
expect(keyInfo:GetUserIds()).toEqual({ 111, 222 })
|
|
110
|
+
end)
|
|
111
|
+
end)
|
|
112
|
+
|
|
113
|
+
describe("DataStoreMock:UpdateAsync", function()
|
|
114
|
+
it("should pass the current value to the transform", function()
|
|
115
|
+
local store = DataStoreMock.new()
|
|
116
|
+
store:SetAsync("key", 1)
|
|
117
|
+
|
|
118
|
+
local seen
|
|
119
|
+
store:UpdateAsync("key", function(current)
|
|
120
|
+
seen = current
|
|
121
|
+
return current + 1
|
|
122
|
+
end)
|
|
123
|
+
|
|
124
|
+
expect(seen).toEqual(1)
|
|
125
|
+
expect((store:GetAsync("key"))).toEqual(2)
|
|
126
|
+
end)
|
|
127
|
+
|
|
128
|
+
it("should pass nil to the transform for an unset key", function()
|
|
129
|
+
local store = DataStoreMock.new()
|
|
130
|
+
|
|
131
|
+
local seen = "unset"
|
|
132
|
+
store:UpdateAsync("key", function(current)
|
|
133
|
+
seen = current
|
|
134
|
+
return 1
|
|
135
|
+
end)
|
|
136
|
+
|
|
137
|
+
expect(seen).toEqual(nil)
|
|
138
|
+
expect((store:GetAsync("key"))).toEqual(1)
|
|
139
|
+
end)
|
|
140
|
+
|
|
141
|
+
it("should return the newly written value", function()
|
|
142
|
+
local store = DataStoreMock.new()
|
|
143
|
+
local written = store:UpdateAsync("key", function()
|
|
144
|
+
return { coins = 3 }
|
|
145
|
+
end)
|
|
146
|
+
expect(written.coins).toEqual(3)
|
|
147
|
+
end)
|
|
148
|
+
|
|
149
|
+
it("should cancel the update when the transform returns nil", function()
|
|
150
|
+
local store = DataStoreMock.new()
|
|
151
|
+
store:SetAsync("key", "original")
|
|
152
|
+
|
|
153
|
+
store:UpdateAsync("key", function()
|
|
154
|
+
return nil
|
|
155
|
+
end)
|
|
156
|
+
|
|
157
|
+
expect((store:GetAsync("key"))).toEqual("original")
|
|
158
|
+
end)
|
|
159
|
+
|
|
160
|
+
it("should deep copy the current value so the transform cannot mutate storage in place", function()
|
|
161
|
+
local store = DataStoreMock.new()
|
|
162
|
+
store:SetAsync("key", { coins = 5 })
|
|
163
|
+
|
|
164
|
+
store:UpdateAsync("key", function(current)
|
|
165
|
+
current.coins = 999
|
|
166
|
+
return nil -- cancel, so only an in-place mutation could leak
|
|
167
|
+
end)
|
|
168
|
+
|
|
169
|
+
expect((store:GetAsync("key")).coins).toEqual(5)
|
|
170
|
+
end)
|
|
171
|
+
|
|
172
|
+
it("should pass a keyInfo to the transform", function()
|
|
173
|
+
local store = DataStoreMock.new()
|
|
174
|
+
store:SetAsync("key", 1, { 42 })
|
|
175
|
+
|
|
176
|
+
local seenUserIds
|
|
177
|
+
store:UpdateAsync("key", function(_current, keyInfo)
|
|
178
|
+
seenUserIds = keyInfo:GetUserIds()
|
|
179
|
+
return 2
|
|
180
|
+
end)
|
|
181
|
+
|
|
182
|
+
expect(seenUserIds).toEqual({ 42 })
|
|
183
|
+
end)
|
|
184
|
+
end)
|
|
185
|
+
|
|
186
|
+
describe("DataStoreMock:RemoveAsync", function()
|
|
187
|
+
it("should return the removed value and clear the key", function()
|
|
188
|
+
local store = DataStoreMock.new()
|
|
189
|
+
store:SetAsync("key", "value")
|
|
190
|
+
|
|
191
|
+
expect((store:RemoveAsync("key"))).toEqual("value")
|
|
192
|
+
expect((store:GetAsync("key"))).toEqual(nil)
|
|
193
|
+
end)
|
|
194
|
+
|
|
195
|
+
it("should return nil when removing a missing key", function()
|
|
196
|
+
local store = DataStoreMock.new()
|
|
197
|
+
expect((store:RemoveAsync("missing"))).toEqual(nil)
|
|
198
|
+
end)
|
|
199
|
+
end)
|
|
200
|
+
|
|
201
|
+
describe("DataStoreMock:IncrementAsync", function()
|
|
202
|
+
it("should increment from zero", function()
|
|
203
|
+
local store = DataStoreMock.new()
|
|
204
|
+
expect(store:IncrementAsync("key", 5)).toEqual(5)
|
|
205
|
+
expect(store:IncrementAsync("key", 3)).toEqual(8)
|
|
206
|
+
end)
|
|
207
|
+
|
|
208
|
+
it("should default the delta to 1", function()
|
|
209
|
+
local store = DataStoreMock.new()
|
|
210
|
+
expect(store:IncrementAsync("key")).toEqual(1)
|
|
211
|
+
end)
|
|
212
|
+
|
|
213
|
+
it("should throw when incrementing a non-number value", function()
|
|
214
|
+
local store = DataStoreMock.new()
|
|
215
|
+
store:SetAsync("key", "not a number")
|
|
216
|
+
expect(function()
|
|
217
|
+
store:IncrementAsync("key", 1)
|
|
218
|
+
end).toThrow("Cannot increment non-number value")
|
|
219
|
+
end)
|
|
220
|
+
end)
|
|
221
|
+
|
|
222
|
+
describe("DataStoreMock failure injection", function()
|
|
223
|
+
it("should throw on every request while failing all requests", function()
|
|
224
|
+
local store = DataStoreMock.new()
|
|
225
|
+
store:FailAllRequests()
|
|
226
|
+
|
|
227
|
+
expect(function()
|
|
228
|
+
store:GetAsync("key")
|
|
229
|
+
end).toThrow("509")
|
|
230
|
+
expect(function()
|
|
231
|
+
store:UpdateAsync("key", function()
|
|
232
|
+
return 1
|
|
233
|
+
end)
|
|
234
|
+
end).toThrow("509")
|
|
235
|
+
expect(function()
|
|
236
|
+
store:SetAsync("key", 1)
|
|
237
|
+
end).toThrow("509")
|
|
238
|
+
expect(function()
|
|
239
|
+
store:RemoveAsync("key")
|
|
240
|
+
end).toThrow("509")
|
|
241
|
+
end)
|
|
242
|
+
|
|
243
|
+
it("should throw the 509 message by default", function()
|
|
244
|
+
local store = DataStoreMock.new()
|
|
245
|
+
store:FailAllRequests()
|
|
246
|
+
|
|
247
|
+
local ok, err = pcall(function()
|
|
248
|
+
store:GetAsync("key")
|
|
249
|
+
end)
|
|
250
|
+
expect(ok).toEqual(false)
|
|
251
|
+
expect(string.find(tostring(err), "509", 1, true) ~= nil).toEqual(true)
|
|
252
|
+
end)
|
|
253
|
+
|
|
254
|
+
it("should throw a custom message when provided", function()
|
|
255
|
+
local store = DataStoreMock.new()
|
|
256
|
+
store:FailAllRequests("custom boom")
|
|
257
|
+
|
|
258
|
+
local ok, err = pcall(function()
|
|
259
|
+
store:GetAsync("key")
|
|
260
|
+
end)
|
|
261
|
+
expect(ok).toEqual(false)
|
|
262
|
+
expect(string.find(tostring(err), "custom boom", 1, true) ~= nil).toEqual(true)
|
|
263
|
+
end)
|
|
264
|
+
|
|
265
|
+
it("should recover after StopFailing", function()
|
|
266
|
+
local store = DataStoreMock.new()
|
|
267
|
+
store:FailAllRequests()
|
|
268
|
+
store:StopFailing()
|
|
269
|
+
|
|
270
|
+
store:SetAsync("key", 1)
|
|
271
|
+
expect((store:GetAsync("key"))).toEqual(1)
|
|
272
|
+
end)
|
|
273
|
+
|
|
274
|
+
it("should fail only the next N requests then recover", function()
|
|
275
|
+
local store = DataStoreMock.new()
|
|
276
|
+
store:FailNextRequests(2)
|
|
277
|
+
|
|
278
|
+
expect(function()
|
|
279
|
+
store:GetAsync("key")
|
|
280
|
+
end).toThrow("509")
|
|
281
|
+
expect(function()
|
|
282
|
+
store:GetAsync("key")
|
|
283
|
+
end).toThrow("509")
|
|
284
|
+
|
|
285
|
+
-- Third request succeeds
|
|
286
|
+
store:SetAsync("key", "ok")
|
|
287
|
+
expect((store:GetAsync("key"))).toEqual("ok")
|
|
288
|
+
end)
|
|
289
|
+
|
|
290
|
+
it("should not fail when FailNextRequests count is zero", function()
|
|
291
|
+
local store = DataStoreMock.new()
|
|
292
|
+
store:FailNextRequests(0)
|
|
293
|
+
store:SetAsync("key", 1)
|
|
294
|
+
expect((store:GetAsync("key"))).toEqual(1)
|
|
295
|
+
end)
|
|
296
|
+
|
|
297
|
+
it("should support a custom error injector targeting a specific method", function()
|
|
298
|
+
local store = DataStoreMock.new()
|
|
299
|
+
store:SetErrorInjector(function(ctx)
|
|
300
|
+
if ctx.method == "UpdateAsync" then
|
|
301
|
+
return "no updates allowed"
|
|
302
|
+
end
|
|
303
|
+
return nil
|
|
304
|
+
end)
|
|
305
|
+
|
|
306
|
+
-- Reads are fine
|
|
307
|
+
expect(function()
|
|
308
|
+
store:GetAsync("key")
|
|
309
|
+
end).never.toThrow()
|
|
310
|
+
|
|
311
|
+
-- Updates fail
|
|
312
|
+
expect(function()
|
|
313
|
+
store:UpdateAsync("key", function()
|
|
314
|
+
return 1
|
|
315
|
+
end)
|
|
316
|
+
end).toThrow("no updates allowed")
|
|
317
|
+
end)
|
|
318
|
+
|
|
319
|
+
it("should still count failed requests", function()
|
|
320
|
+
local store = DataStoreMock.new()
|
|
321
|
+
store:FailAllRequests()
|
|
322
|
+
|
|
323
|
+
pcall(function()
|
|
324
|
+
store:GetAsync("key")
|
|
325
|
+
end)
|
|
326
|
+
pcall(function()
|
|
327
|
+
store:GetAsync("key")
|
|
328
|
+
end)
|
|
329
|
+
|
|
330
|
+
expect(store:GetCallCount("GetAsync")).toEqual(2)
|
|
331
|
+
expect(store:GetCallCount()).toEqual(2)
|
|
332
|
+
end)
|
|
333
|
+
|
|
334
|
+
it("should not mutate storage when a request fails", function()
|
|
335
|
+
local store = DataStoreMock.new()
|
|
336
|
+
store:SetAsync("key", "original")
|
|
337
|
+
store:FailAllRequests()
|
|
338
|
+
|
|
339
|
+
pcall(function()
|
|
340
|
+
store:SetAsync("key", "should not persist")
|
|
341
|
+
end)
|
|
342
|
+
|
|
343
|
+
store:StopFailing()
|
|
344
|
+
expect((store:GetAsync("key"))).toEqual("original")
|
|
345
|
+
end)
|
|
346
|
+
end)
|
|
347
|
+
|
|
348
|
+
describe("DataStoreMock call counting", function()
|
|
349
|
+
it("should count per-method and in total", function()
|
|
350
|
+
local store = DataStoreMock.new()
|
|
351
|
+
store:GetAsync("a")
|
|
352
|
+
store:GetAsync("b")
|
|
353
|
+
store:SetAsync("a", 1)
|
|
354
|
+
|
|
355
|
+
expect(store:GetCallCount("GetAsync")).toEqual(2)
|
|
356
|
+
expect(store:GetCallCount("SetAsync")).toEqual(1)
|
|
357
|
+
expect(store:GetCallCount("UpdateAsync")).toEqual(0)
|
|
358
|
+
expect(store:GetCallCount()).toEqual(3)
|
|
359
|
+
end)
|
|
360
|
+
end)
|
|
361
|
+
|
|
362
|
+
describe("DataStoreMock:SetRaw / GetRaw", function()
|
|
363
|
+
it("should seed and read without triggering failures", function()
|
|
364
|
+
local store = DataStoreMock.new()
|
|
365
|
+
store:FailAllRequests()
|
|
366
|
+
|
|
367
|
+
store:SetRaw("key", { coins = 7 })
|
|
368
|
+
expect(store:GetRaw("key").coins).toEqual(7)
|
|
369
|
+
-- Raw access does not count as a datastore call
|
|
370
|
+
expect(store:GetCallCount()).toEqual(0)
|
|
371
|
+
end)
|
|
372
|
+
|
|
373
|
+
it("should deep copy on SetRaw and GetRaw", function()
|
|
374
|
+
local store = DataStoreMock.new()
|
|
375
|
+
local seed = { coins = 1 }
|
|
376
|
+
store:SetRaw("key", seed)
|
|
377
|
+
seed.coins = 999
|
|
378
|
+
|
|
379
|
+
local read = store:GetRaw("key")
|
|
380
|
+
read.coins = 555
|
|
381
|
+
|
|
382
|
+
expect(store:GetRaw("key").coins).toEqual(1)
|
|
383
|
+
end)
|
|
384
|
+
end)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
--!nonstrict
|
|
2
|
+
--[[
|
|
3
|
+
Characterization tests for DataStoreSnapshotUtils.
|
|
4
|
+
@class DataStoreSnapshotUtils.spec.lua
|
|
5
|
+
]]
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local DataStoreDeleteToken = require("DataStoreDeleteToken")
|
|
9
|
+
local DataStoreSnapshotUtils = require("DataStoreSnapshotUtils")
|
|
10
|
+
local Jest = require("Jest")
|
|
11
|
+
|
|
12
|
+
local describe = Jest.Globals.describe
|
|
13
|
+
local expect = Jest.Globals.expect
|
|
14
|
+
local it = Jest.Globals.it
|
|
15
|
+
|
|
16
|
+
describe("DataStoreSnapshotUtils.isEmptySnapshot(snapshot)", function()
|
|
17
|
+
it("should return true for an empty table", function()
|
|
18
|
+
expect(DataStoreSnapshotUtils.isEmptySnapshot({})).toEqual(true)
|
|
19
|
+
end)
|
|
20
|
+
|
|
21
|
+
it("should return false for a table with array entries", function()
|
|
22
|
+
expect(DataStoreSnapshotUtils.isEmptySnapshot({ 1, 2, 3 })).toEqual(false)
|
|
23
|
+
end)
|
|
24
|
+
|
|
25
|
+
it("should return false for a table with a single dictionary entry", function()
|
|
26
|
+
expect(DataStoreSnapshotUtils.isEmptySnapshot({ key = "value" })).toEqual(false)
|
|
27
|
+
end)
|
|
28
|
+
|
|
29
|
+
it("should return false for a table with a false value", function()
|
|
30
|
+
expect(DataStoreSnapshotUtils.isEmptySnapshot({ key = false })).toEqual(false)
|
|
31
|
+
end)
|
|
32
|
+
|
|
33
|
+
it("should return false for a nested table", function()
|
|
34
|
+
expect(DataStoreSnapshotUtils.isEmptySnapshot({ nested = {} })).toEqual(false)
|
|
35
|
+
end)
|
|
36
|
+
|
|
37
|
+
it("should return false for a table holding an empty nested table by index", function()
|
|
38
|
+
expect(DataStoreSnapshotUtils.isEmptySnapshot({ {} })).toEqual(false)
|
|
39
|
+
end)
|
|
40
|
+
|
|
41
|
+
it("should return true for a frozen empty table", function()
|
|
42
|
+
expect(DataStoreSnapshotUtils.isEmptySnapshot(table.freeze({}))).toEqual(true)
|
|
43
|
+
end)
|
|
44
|
+
|
|
45
|
+
it("should return false for a frozen non-empty table", function()
|
|
46
|
+
expect(DataStoreSnapshotUtils.isEmptySnapshot(table.freeze({ key = "value" }))).toEqual(false)
|
|
47
|
+
end)
|
|
48
|
+
|
|
49
|
+
it("should return false for the delete token", function()
|
|
50
|
+
expect(DataStoreSnapshotUtils.isEmptySnapshot(DataStoreDeleteToken)).toEqual(false)
|
|
51
|
+
end)
|
|
52
|
+
|
|
53
|
+
it("should return false for nil", function()
|
|
54
|
+
expect(DataStoreSnapshotUtils.isEmptySnapshot(nil)).toEqual(false)
|
|
55
|
+
end)
|
|
56
|
+
|
|
57
|
+
it("should return false for a string", function()
|
|
58
|
+
expect(DataStoreSnapshotUtils.isEmptySnapshot("")).toEqual(false)
|
|
59
|
+
expect(DataStoreSnapshotUtils.isEmptySnapshot("hello")).toEqual(false)
|
|
60
|
+
end)
|
|
61
|
+
|
|
62
|
+
it("should return false for a number", function()
|
|
63
|
+
expect(DataStoreSnapshotUtils.isEmptySnapshot(0)).toEqual(false)
|
|
64
|
+
expect(DataStoreSnapshotUtils.isEmptySnapshot(5)).toEqual(false)
|
|
65
|
+
end)
|
|
66
|
+
|
|
67
|
+
it("should return false for a boolean", function()
|
|
68
|
+
expect(DataStoreSnapshotUtils.isEmptySnapshot(true)).toEqual(false)
|
|
69
|
+
expect(DataStoreSnapshotUtils.isEmptySnapshot(false)).toEqual(false)
|
|
70
|
+
end)
|
|
71
|
+
|
|
72
|
+
it("should return false for a function", function()
|
|
73
|
+
expect(DataStoreSnapshotUtils.isEmptySnapshot(function() end)).toEqual(false)
|
|
74
|
+
end)
|
|
75
|
+
|
|
76
|
+
it("should return a boolean type, never nil", function()
|
|
77
|
+
expect(DataStoreSnapshotUtils.isEmptySnapshot({})).never.toBeNil()
|
|
78
|
+
expect(DataStoreSnapshotUtils.isEmptySnapshot(nil)).never.toBeNil()
|
|
79
|
+
expect(type(DataStoreSnapshotUtils.isEmptySnapshot({}))).toEqual("boolean")
|
|
80
|
+
expect(type(DataStoreSnapshotUtils.isEmptySnapshot(nil))).toEqual("boolean")
|
|
81
|
+
end)
|
|
82
|
+
|
|
83
|
+
it("should not throw for any input type", function()
|
|
84
|
+
expect(function()
|
|
85
|
+
DataStoreSnapshotUtils.isEmptySnapshot(nil)
|
|
86
|
+
DataStoreSnapshotUtils.isEmptySnapshot({})
|
|
87
|
+
DataStoreSnapshotUtils.isEmptySnapshot(DataStoreDeleteToken)
|
|
88
|
+
DataStoreSnapshotUtils.isEmptySnapshot("string")
|
|
89
|
+
end).never.toThrow()
|
|
90
|
+
end)
|
|
91
|
+
end)
|
|
@@ -710,8 +710,14 @@ function DataStoreStage.PromiseInvokeSavingCallbacks(self: DataStoreStage)
|
|
|
710
710
|
local removingPromises: { Promise.Promise<()> } = {}
|
|
711
711
|
|
|
712
712
|
for _, func in self._savingCallbacks do
|
|
713
|
-
|
|
714
|
-
|
|
713
|
+
-- Isolate the callback so a throw fails the save cleanly, preserving the stack trace.
|
|
714
|
+
local ok, result = xpcall(func, function(err)
|
|
715
|
+
return debug.traceback(tostring(err), 2)
|
|
716
|
+
end)
|
|
717
|
+
if not ok then
|
|
718
|
+
warn(`[DataStoreStage] - Saving callback errored: {result}`)
|
|
719
|
+
table.insert(removingPromises, Promise.rejected(result) :: any)
|
|
720
|
+
elseif Promise.isPromise(result) then
|
|
715
721
|
table.insert(removingPromises, result :: any)
|
|
716
722
|
end
|
|
717
723
|
end
|