@quenty/datastore 13.40.0 → 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.
Files changed (32) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/package.json +9 -9
  3. package/src/Server/DataStore.HookErrors.spec.lua +92 -0
  4. package/src/Server/DataStore.LoadErrors.spec.lua +133 -0
  5. package/src/Server/DataStore.Reentrance.spec.lua +119 -0
  6. package/src/Server/DataStore.SessionLock.spec.lua +478 -0
  7. package/src/Server/DataStore.SessionMessaging.spec.lua +71 -0
  8. package/src/Server/DataStore.TwoServerLock.spec.lua +190 -0
  9. package/src/Server/DataStore.lua +89 -18
  10. package/src/Server/DataStore.spec.lua +237 -0
  11. package/src/Server/DataStoreLockHelper.spec.lua +239 -0
  12. package/src/Server/DataStoreMessageHelper.spec.lua +134 -0
  13. package/src/Server/DataStoreNonRetryableLoadError.lua +61 -0
  14. package/src/Server/DataStoreTestUtils.lua +206 -0
  15. package/src/Server/GameDataStoreService.lua +26 -0
  16. package/src/Server/GameDataStoreService.spec.lua +215 -0
  17. package/src/Server/Mocks/DataStoreMock.lua +405 -0
  18. package/src/Server/Mocks/DataStoreMock.spec.lua +384 -0
  19. package/src/Server/Modules/DataStoreSnapshotUtils.spec.lua +91 -0
  20. package/src/Server/Modules/DataStoreStage.lua +8 -2
  21. package/src/Server/Modules/DataStoreStage.spec.lua +539 -0
  22. package/src/Server/Modules/DataStoreWriter.spec.lua +399 -0
  23. package/src/Server/PlayerDataStoreManager.RemovalCallbacks.spec.lua +99 -0
  24. package/src/Server/PlayerDataStoreManager.lua +36 -4
  25. package/src/Server/PlayerDataStoreManager.spec.lua +202 -0
  26. package/src/Server/PlayerDataStoreService.lua +18 -0
  27. package/src/Server/PlayerDataStoreService.spec.lua +218 -0
  28. package/src/Server/PrivateServerDataStoreService.lua +23 -1
  29. package/src/Server/PrivateServerDataStoreService.spec.lua +242 -0
  30. package/src/Server/Utility/DataStorePromises.lua +18 -5
  31. package/src/Server/Utility/DataStorePromises.spec.lua +363 -0
  32. package/src/jest.config.lua +3 -0
@@ -0,0 +1,242 @@
1
+ --!nonstrict
2
+ --[[
3
+ Integration coverage for PrivateServerDataStoreService wired through a real ServiceBag, with the
4
+ underlying datastore injected via the SetRobloxDataStore test seam. It is not session-locking, so a
5
+ failing load rejects promptly rather than hanging. The datastore key defaults to "main" (or the
6
+ private-server id) unless a custom key is set via SetCustomKey before the datastore is resolved.
7
+
8
+ @class PrivateServerDataStoreService.spec.lua
9
+ ]]
10
+ local require = require(script.Parent.loader).load(script)
11
+
12
+ local DataStoreMock = require("DataStoreMock")
13
+ local Jest = require("Jest")
14
+ local Maid = require("Maid")
15
+ local PromiseTestUtils = require("PromiseTestUtils")
16
+ local ServiceBag = require("ServiceBag")
17
+
18
+ local describe = Jest.Globals.describe
19
+ local expect = Jest.Globals.expect
20
+ local it = Jest.Globals.it
21
+
22
+ local function setup(mock)
23
+ local maid = Maid.new()
24
+ mock = mock or DataStoreMock.new()
25
+
26
+ local serviceBag = maid:Add(ServiceBag.new())
27
+ local service = serviceBag:GetService(require("PrivateServerDataStoreService"))
28
+ serviceBag:Init()
29
+ service:SetRobloxDataStore(mock)
30
+ serviceBag:Start()
31
+
32
+ return {
33
+ service = service,
34
+ mock = mock,
35
+ destroy = function()
36
+ maid:DoCleaning()
37
+ end,
38
+ }
39
+ end
40
+
41
+ describe("PrivateServerDataStoreService.PromiseDataStore", function()
42
+ it("should resolve a datastore that loads successfully against a healthy mock", function()
43
+ local controller = setup()
44
+
45
+ local promise = controller.service:PromiseDataStore()
46
+ if not PromiseTestUtils.awaitSettled(promise) then
47
+ expect("hung").toEqual("settled")
48
+ controller:destroy()
49
+ return
50
+ end
51
+
52
+ local ok, dataStore = promise:Yield()
53
+ expect(ok).toEqual(true)
54
+ expect(dataStore).never.toBeNil()
55
+
56
+ local loadPromise = dataStore:PromiseLoadSuccessful()
57
+ if not PromiseTestUtils.awaitSettled(loadPromise) then
58
+ expect("hung").toEqual("settled")
59
+ controller:destroy()
60
+ return
61
+ end
62
+ expect((loadPromise:Wait())).toEqual(true)
63
+
64
+ controller:destroy()
65
+ end)
66
+
67
+ it("should return the same cached promise on repeated calls", function()
68
+ local controller = setup()
69
+
70
+ local first = controller.service:PromiseDataStore()
71
+ local second = controller.service:PromiseDataStore()
72
+ expect((first == second)).toEqual(true)
73
+
74
+ controller:destroy()
75
+ end)
76
+ end)
77
+
78
+ describe("PrivateServerDataStoreService.SetCustomKey", function()
79
+ it("should key the datastore by the custom key when set before resolving", function()
80
+ local controller = setup()
81
+
82
+ controller.service:SetCustomKey("mykey")
83
+
84
+ local promise = controller.service:PromiseDataStore()
85
+ if not PromiseTestUtils.awaitSettled(promise) then
86
+ expect("hung").toEqual("settled")
87
+ controller:destroy()
88
+ return
89
+ end
90
+
91
+ local ok, dataStore = promise:Yield()
92
+ expect(ok).toEqual(true)
93
+ expect((dataStore:GetKey())).toEqual("mykey")
94
+
95
+ controller:destroy()
96
+ end)
97
+ end)
98
+
99
+ describe("PrivateServerDataStoreService persistence", function()
100
+ it("should round-trip a stored value into the mock under the datastore key", function()
101
+ local controller = setup()
102
+
103
+ controller.service:SetCustomKey("mykey")
104
+
105
+ local promise = controller.service:PromiseDataStore()
106
+ if not PromiseTestUtils.awaitSettled(promise) then
107
+ expect("hung").toEqual("settled")
108
+ controller:destroy()
109
+ return
110
+ end
111
+
112
+ local ok, dataStore = promise:Yield()
113
+ expect(ok).toEqual(true)
114
+
115
+ local key = dataStore:GetKey()
116
+ dataStore:Store("motd", "hello")
117
+
118
+ local savePromise = dataStore:Save()
119
+ if not PromiseTestUtils.awaitSettled(savePromise) then
120
+ expect("hung").toEqual("settled")
121
+ controller:destroy()
122
+ return
123
+ end
124
+ expect((savePromise:Yield())).toEqual(true)
125
+
126
+ -- Non-session-locking, so the raw persisted value is the plain data table.
127
+ local raw = controller.mock:GetRaw(key)
128
+ expect(raw).never.toBeNil()
129
+ expect(raw.motd).toEqual("hello")
130
+
131
+ controller:destroy()
132
+ end)
133
+ end)
134
+
135
+ describe("PrivateServerDataStoreService.SetRobloxDataStore", function()
136
+ it("should throw when injected twice (already resolved)", function()
137
+ local controller = setup()
138
+
139
+ expect(function()
140
+ controller.service:SetRobloxDataStore(DataStoreMock.new())
141
+ end).toThrow("Already resolved robloxDataStore")
142
+
143
+ controller:destroy()
144
+ end)
145
+
146
+ it("should throw on a non-datastore argument", function()
147
+ local controller = setup()
148
+
149
+ -- isDataStore is validated before the already-resolved check, so a bad arg throws regardless.
150
+ expect(function()
151
+ controller.service:SetRobloxDataStore({})
152
+ end).toThrow("Bad robloxDataStore")
153
+
154
+ expect(function()
155
+ controller.service:SetRobloxDataStore(nil)
156
+ end).toThrow("Bad robloxDataStore")
157
+
158
+ controller:destroy()
159
+ end)
160
+ end)
161
+
162
+ describe("PrivateServerDataStoreService failure handling", function()
163
+ it("should resolve load as false (not hang) when the datastore is down", function()
164
+ local mock = DataStoreMock.new()
165
+ mock:FailAllRequests()
166
+
167
+ local controller = setup(mock)
168
+
169
+ local promise = controller.service:PromiseDataStore()
170
+ if not PromiseTestUtils.awaitSettled(promise, 5) then
171
+ expect("hung").toEqual("settled")
172
+ controller:destroy()
173
+ return
174
+ end
175
+
176
+ local ok, dataStore = promise:Yield()
177
+ expect(ok).toEqual(true)
178
+
179
+ -- Non-session-locking: a failing load rejects promptly, so this settles false rather than hanging.
180
+ local loadPromise = dataStore:PromiseLoadSuccessful()
181
+ if not PromiseTestUtils.awaitSettled(loadPromise, 5) then
182
+ expect("hung").toEqual("settled")
183
+ controller:destroy()
184
+ return
185
+ end
186
+
187
+ local loadOk, loadedOk = loadPromise:Yield()
188
+ expect(loadOk).toEqual(true)
189
+ expect(loadedOk).toEqual(false)
190
+
191
+ controller:destroy()
192
+ end)
193
+ end)
194
+
195
+ describe("PrivateServerDataStoreService teardown", function()
196
+ it("destroys its inner datastore when the service is destroyed", function()
197
+ local controller = setup()
198
+
199
+ local promise = controller.service:PromiseDataStore()
200
+ if not PromiseTestUtils.awaitSettled(promise, 5) then
201
+ expect("hung").toEqual("settled")
202
+ controller:destroy()
203
+ return
204
+ end
205
+ local _ok, dataStore = promise:Yield()
206
+
207
+ controller:destroy()
208
+
209
+ expect(getmetatable(dataStore)).toBeNil()
210
+ end)
211
+
212
+ it("flushes staged data synchronously to the underlying store when destroyed", function()
213
+ local controller = setup()
214
+
215
+ -- The cloud test place has a PrivateServerId, so pin the key rather than assuming "main".
216
+ controller.service:SetCustomKey("mykey")
217
+
218
+ local promise = controller.service:PromiseDataStore()
219
+ if not PromiseTestUtils.awaitSettled(promise, 5) then
220
+ expect("hung").toEqual("settled")
221
+ controller:destroy()
222
+ return
223
+ end
224
+ local _ok, dataStore = promise:Yield()
225
+
226
+ if not PromiseTestUtils.awaitSettled(dataStore:PromiseLoadSuccessful(), 5) then
227
+ expect("load hung").toEqual("load settled")
228
+ controller:destroy()
229
+ return
230
+ end
231
+
232
+ local key = dataStore:GetKey()
233
+ dataStore:Store("region", "us")
234
+
235
+ -- Destroy must fire a synchronous Save before tearing the store down.
236
+ controller:destroy()
237
+
238
+ local raw = controller.mock:GetRaw(key)
239
+ expect(raw).never.toBeNil()
240
+ expect(raw.region).toEqual("us")
241
+ end)
242
+ end)
@@ -9,6 +9,7 @@ local require = require(script.Parent.loader).load(script)
9
9
 
10
10
  local DataStoreService = game:GetService("DataStoreService")
11
11
 
12
+ local DataStoreMock = require("DataStoreMock")
12
13
  local PagesUtils = require("PagesUtils")
13
14
  local Promise = require("Promise")
14
15
  local Table = require("Table")
@@ -17,6 +18,18 @@ local DataStorePromises = {}
17
18
 
18
19
  export type RobloxDataStore = DataStore
19
20
 
21
+ --[=[
22
+ Returns whether the given value can be used as a datastore by these helpers. A real
23
+ datastore is an `Instance`, but the first-class [DataStoreMock] is also accepted so tests
24
+ can stand in for a real datastore.
25
+
26
+ @param robloxDataStore any
27
+ @return boolean
28
+ ]=]
29
+ function DataStorePromises.isDataStore(robloxDataStore: any): boolean
30
+ return typeof(robloxDataStore) == "Instance" or DataStoreMock.isDataStoreMock(robloxDataStore)
31
+ end
32
+
20
33
  --[=[
21
34
  Promises a Roblox datastore object with the name and scope. Generally only fails
22
35
  when you haven't published the place.
@@ -70,7 +83,7 @@ end
70
83
  @return Promise<T>
71
84
  ]=]
72
85
  function DataStorePromises.getAsync<T>(robloxDataStore: DataStore, key: string): Promise.Promise<T>
73
- assert(typeof(robloxDataStore) == "Instance", "Bad robloxDataStore")
86
+ assert(DataStorePromises.isDataStore(robloxDataStore), "Bad robloxDataStore")
74
87
  assert(type(key) == "string", "Bad key")
75
88
 
76
89
  return Promise.spawn(function(resolve, reject)
@@ -99,7 +112,7 @@ function DataStorePromises.updateAsync<T>(
99
112
  key: string,
100
113
  updateFunc: (T, DataStoreKeyInfo) -> T?
101
114
  ): Promise.Promise<(T, DataStoreKeyInfo)>
102
- assert(typeof(robloxDataStore) == "Instance", "Bad robloxDataStore")
115
+ assert(DataStorePromises.isDataStore(robloxDataStore), "Bad robloxDataStore")
103
116
  assert(type(key) == "string", "Bad key")
104
117
  assert(type(updateFunc) == "function", "Bad updateFunc")
105
118
 
@@ -132,7 +145,7 @@ function DataStorePromises.setAsync(
132
145
  value: string,
133
146
  userIds: { number }?
134
147
  ): Promise.Promise<boolean>
135
- assert(typeof(robloxDataStore) == "Instance", "Bad robloxDataStore")
148
+ assert(DataStorePromises.isDataStore(robloxDataStore), "Bad robloxDataStore")
136
149
  assert(type(key) == "string", "Bad key")
137
150
  assert(type(userIds) == "table" or userIds == nil, "Bad userIds")
138
151
 
@@ -159,7 +172,7 @@ function DataStorePromises.promiseIncrementAsync(
159
172
  key: string,
160
173
  delta: number
161
174
  ): Promise.Promise<boolean>
162
- assert(typeof(robloxDataStore) == "Instance", "Bad robloxDataStore")
175
+ assert(DataStorePromises.isDataStore(robloxDataStore), "Bad robloxDataStore")
163
176
  assert(type(key) == "string", "Bad key")
164
177
  assert(type(delta) == "number" or delta == nil, "Bad delta")
165
178
 
@@ -181,7 +194,7 @@ end
181
194
  @return Promise<boolean>
182
195
  ]=]
183
196
  function DataStorePromises.removeAsync(robloxDataStore: DataStore, key: string): Promise.Promise<boolean>
184
- assert(typeof(robloxDataStore) == "Instance", "Bad robloxDataStore")
197
+ assert(DataStorePromises.isDataStore(robloxDataStore), "Bad robloxDataStore")
185
198
  assert(type(key) == "string", "Bad key")
186
199
 
187
200
  return Promise.spawn(function(resolve, reject)
@@ -0,0 +1,363 @@
1
+ --!nonstrict
2
+ --[[
3
+ Characterization coverage for DataStorePromises, using DataStoreMock as the datastore.
4
+ @class DataStorePromises.spec.lua
5
+ ]]
6
+ local require = require(script.Parent.loader).load(script)
7
+
8
+ local DataStoreMock = require("DataStoreMock")
9
+ local DataStorePromises = require("DataStorePromises")
10
+ local Jest = require("Jest")
11
+ local PromiseTestUtils = require("PromiseTestUtils")
12
+
13
+ local describe = Jest.Globals.describe
14
+ local expect = Jest.Globals.expect
15
+ local it = Jest.Globals.it
16
+
17
+ describe("DataStorePromises.isDataStore", function()
18
+ it("should be true for a DataStoreMock", function()
19
+ expect(DataStorePromises.isDataStore(DataStoreMock.new())).toEqual(true)
20
+ end)
21
+
22
+ it("should be false for a plain table", function()
23
+ expect(DataStorePromises.isDataStore({})).toEqual(false)
24
+ end)
25
+
26
+ it("should be false for nil", function()
27
+ expect(DataStorePromises.isDataStore(nil)).toEqual(false)
28
+ end)
29
+
30
+ it("should be false for primitives", function()
31
+ expect(DataStorePromises.isDataStore(5)).toEqual(false)
32
+ expect(DataStorePromises.isDataStore("str")).toEqual(false)
33
+ expect(DataStorePromises.isDataStore(true)).toEqual(false)
34
+ end)
35
+
36
+ it("should be true for an Instance", function()
37
+ expect(DataStorePromises.isDataStore(Instance.new("Folder"))).toEqual(true)
38
+ end)
39
+ end)
40
+
41
+ describe("DataStorePromises.promiseDataStore", function()
42
+ it("should resolve an Instance in the cloud test place", function()
43
+ local promise = DataStorePromises.promiseDataStore("SpecStore", "SpecScope")
44
+ if not PromiseTestUtils.awaitSettled(promise) then
45
+ expect("promise hung").toEqual("promise settled")
46
+ return
47
+ end
48
+
49
+ local ok, dataStore = promise:Yield()
50
+ expect(ok).toEqual(true)
51
+ expect(typeof(dataStore)).toEqual("Instance")
52
+ end)
53
+
54
+ it("should throw synchronously for a non-string name", function()
55
+ expect(function()
56
+ DataStorePromises.promiseDataStore(5 :: any, "scope")
57
+ end).toThrow("Bad name")
58
+ end)
59
+
60
+ it("should throw synchronously for a non-string scope", function()
61
+ expect(function()
62
+ DataStorePromises.promiseDataStore("name", 5 :: any)
63
+ end).toThrow("Bad scope")
64
+ end)
65
+ end)
66
+
67
+ describe("DataStorePromises.getAsync", function()
68
+ it("should resolve with the stored value", function()
69
+ local mock = DataStoreMock.new()
70
+ mock:SetRaw("key", { coins = 5 })
71
+
72
+ local promise = DataStorePromises.getAsync(mock, "key")
73
+ if not PromiseTestUtils.awaitSettled(promise) then
74
+ expect("promise hung").toEqual("promise settled")
75
+ return
76
+ end
77
+
78
+ local ok, value = promise:Yield()
79
+ expect(ok).toEqual(true)
80
+ expect(value.coins).toEqual(5)
81
+ end)
82
+
83
+ it("should resolve with nil for an unset key", function()
84
+ local mock = DataStoreMock.new()
85
+
86
+ local promise = DataStorePromises.getAsync(mock, "missing")
87
+ if not PromiseTestUtils.awaitSettled(promise) then
88
+ expect("promise hung").toEqual("promise settled")
89
+ return
90
+ end
91
+
92
+ local ok, value = promise:Yield()
93
+ expect(ok).toEqual(true)
94
+ expect(value).toEqual(nil)
95
+ end)
96
+
97
+ it("should reject when the store fails all requests", function()
98
+ local mock = DataStoreMock.new()
99
+ mock:FailAllRequests()
100
+
101
+ local promise = DataStorePromises.getAsync(mock, "key")
102
+ if not PromiseTestUtils.awaitSettled(promise) then
103
+ expect("promise hung").toEqual("promise settled")
104
+ return
105
+ end
106
+
107
+ local ok = promise:Yield()
108
+ expect(ok).toEqual(false)
109
+ end)
110
+
111
+ it("should throw synchronously for a bad robloxDataStore", function()
112
+ expect(function()
113
+ DataStorePromises.getAsync({} :: any, "key")
114
+ end).toThrow("Bad robloxDataStore")
115
+ end)
116
+
117
+ it("should throw synchronously for a non-string key", function()
118
+ local mock = DataStoreMock.new()
119
+ expect(function()
120
+ DataStorePromises.getAsync(mock, 5 :: any)
121
+ end).toThrow("Bad key")
122
+ end)
123
+ end)
124
+
125
+ describe("DataStorePromises.updateAsync", function()
126
+ it("should resolve with the updated value", function()
127
+ local mock = DataStoreMock.new()
128
+ mock:SetRaw("key", 1)
129
+
130
+ local promise = DataStorePromises.updateAsync(mock, "key", function(current)
131
+ return (current or 0) + 1
132
+ end)
133
+ if not PromiseTestUtils.awaitSettled(promise) then
134
+ expect("promise hung").toEqual("promise settled")
135
+ return
136
+ end
137
+
138
+ local ok, value = promise:Yield()
139
+ expect(ok).toEqual(true)
140
+ expect(value).toEqual(2)
141
+ end)
142
+
143
+ it("should pass the current value to the transform and update the store", function()
144
+ local mock = DataStoreMock.new()
145
+ mock:SetRaw("key", 10)
146
+
147
+ local seen
148
+ local promise = DataStorePromises.updateAsync(mock, "key", function(current)
149
+ seen = current
150
+ return current + 5
151
+ end)
152
+ if not PromiseTestUtils.awaitSettled(promise) then
153
+ expect("promise hung").toEqual("promise settled")
154
+ return
155
+ end
156
+
157
+ local ok = promise:Yield()
158
+ expect(ok).toEqual(true)
159
+ expect(seen).toEqual(10)
160
+ expect(mock:GetRaw("key")).toEqual(15)
161
+ end)
162
+
163
+ it("should reject when the store fails all requests", function()
164
+ local mock = DataStoreMock.new()
165
+ mock:FailAllRequests()
166
+
167
+ local promise = DataStorePromises.updateAsync(mock, "key", function()
168
+ return 1
169
+ end)
170
+ if not PromiseTestUtils.awaitSettled(promise) then
171
+ expect("promise hung").toEqual("promise settled")
172
+ return
173
+ end
174
+
175
+ local ok = promise:Yield()
176
+ expect(ok).toEqual(false)
177
+ end)
178
+
179
+ it("should throw synchronously for a bad robloxDataStore", function()
180
+ expect(function()
181
+ DataStorePromises.updateAsync({} :: any, "key", function()
182
+ return 1
183
+ end)
184
+ end).toThrow("Bad robloxDataStore")
185
+ end)
186
+
187
+ it("should throw synchronously for a non-string key", function()
188
+ local mock = DataStoreMock.new()
189
+ expect(function()
190
+ DataStorePromises.updateAsync(mock, 5 :: any, function()
191
+ return 1
192
+ end)
193
+ end).toThrow("Bad key")
194
+ end)
195
+
196
+ it("should throw synchronously for a non-function updateFunc", function()
197
+ local mock = DataStoreMock.new()
198
+ expect(function()
199
+ DataStorePromises.updateAsync(mock, "key", 5 :: any)
200
+ end).toThrow("Bad updateFunc")
201
+ end)
202
+ end)
203
+
204
+ describe("DataStorePromises.setAsync", function()
205
+ it("should resolve true and write the value", function()
206
+ local mock = DataStoreMock.new()
207
+
208
+ local promise = DataStorePromises.setAsync(mock, "key", "value")
209
+ if not PromiseTestUtils.awaitSettled(promise) then
210
+ expect("promise hung").toEqual("promise settled")
211
+ return
212
+ end
213
+
214
+ local ok, value = promise:Yield()
215
+ expect(ok).toEqual(true)
216
+ expect(value).toEqual(true)
217
+ expect(mock:GetRaw("key")).toEqual("value")
218
+ end)
219
+
220
+ it("should accept userIds", function()
221
+ local mock = DataStoreMock.new()
222
+
223
+ local promise = DataStorePromises.setAsync(mock, "key", "value", { 111, 222 })
224
+ if not PromiseTestUtils.awaitSettled(promise) then
225
+ expect("promise hung").toEqual("promise settled")
226
+ return
227
+ end
228
+
229
+ local ok = promise:Yield()
230
+ expect(ok).toEqual(true)
231
+ end)
232
+
233
+ it("should reject when the store fails all requests", function()
234
+ local mock = DataStoreMock.new()
235
+ mock:FailAllRequests()
236
+
237
+ local promise = DataStorePromises.setAsync(mock, "key", "value")
238
+ if not PromiseTestUtils.awaitSettled(promise) then
239
+ expect("promise hung").toEqual("promise settled")
240
+ return
241
+ end
242
+
243
+ local ok = promise:Yield()
244
+ expect(ok).toEqual(false)
245
+ end)
246
+
247
+ it("should throw synchronously for a bad robloxDataStore", function()
248
+ expect(function()
249
+ DataStorePromises.setAsync({} :: any, "key", "value")
250
+ end).toThrow("Bad robloxDataStore")
251
+ end)
252
+
253
+ it("should throw synchronously for a non-string key", function()
254
+ local mock = DataStoreMock.new()
255
+ expect(function()
256
+ DataStorePromises.setAsync(mock, 5 :: any, "value")
257
+ end).toThrow("Bad key")
258
+ end)
259
+
260
+ it("should throw synchronously for bad userIds", function()
261
+ local mock = DataStoreMock.new()
262
+ expect(function()
263
+ DataStorePromises.setAsync(mock, "key", "value", "notatable" :: any)
264
+ end).toThrow("Bad userIds")
265
+ end)
266
+ end)
267
+
268
+ describe("DataStorePromises.promiseIncrementAsync", function()
269
+ it("should resolve true and increment the value", function()
270
+ local mock = DataStoreMock.new()
271
+ mock:SetRaw("key", 5)
272
+
273
+ local promise = DataStorePromises.promiseIncrementAsync(mock, "key", 3)
274
+ if not PromiseTestUtils.awaitSettled(promise) then
275
+ expect("promise hung").toEqual("promise settled")
276
+ return
277
+ end
278
+
279
+ local ok, value = promise:Yield()
280
+ expect(ok).toEqual(true)
281
+ expect(value).toEqual(true)
282
+ expect(mock:GetRaw("key")).toEqual(8)
283
+ end)
284
+
285
+ it("should reject when the store fails all requests", function()
286
+ local mock = DataStoreMock.new()
287
+ mock:FailAllRequests()
288
+
289
+ local promise = DataStorePromises.promiseIncrementAsync(mock, "key", 1)
290
+ if not PromiseTestUtils.awaitSettled(promise) then
291
+ expect("promise hung").toEqual("promise settled")
292
+ return
293
+ end
294
+
295
+ local ok = promise:Yield()
296
+ expect(ok).toEqual(false)
297
+ end)
298
+
299
+ it("should throw synchronously for a bad robloxDataStore", function()
300
+ expect(function()
301
+ DataStorePromises.promiseIncrementAsync({} :: any, "key", 1)
302
+ end).toThrow("Bad robloxDataStore")
303
+ end)
304
+
305
+ it("should throw synchronously for a non-string key", function()
306
+ local mock = DataStoreMock.new()
307
+ expect(function()
308
+ DataStorePromises.promiseIncrementAsync(mock, 5 :: any, 1)
309
+ end).toThrow("Bad key")
310
+ end)
311
+
312
+ it("should throw synchronously for a non-number delta", function()
313
+ local mock = DataStoreMock.new()
314
+ expect(function()
315
+ DataStorePromises.promiseIncrementAsync(mock, "key", "notanumber" :: any)
316
+ end).toThrow("Bad delta")
317
+ end)
318
+ end)
319
+
320
+ describe("DataStorePromises.removeAsync", function()
321
+ it("should resolve true and clear the key", function()
322
+ local mock = DataStoreMock.new()
323
+ mock:SetRaw("key", "value")
324
+
325
+ local promise = DataStorePromises.removeAsync(mock, "key")
326
+ if not PromiseTestUtils.awaitSettled(promise) then
327
+ expect("promise hung").toEqual("promise settled")
328
+ return
329
+ end
330
+
331
+ local ok, value = promise:Yield()
332
+ expect(ok).toEqual(true)
333
+ expect(value).toEqual(true)
334
+ expect(mock:GetRaw("key")).toEqual(nil)
335
+ end)
336
+
337
+ it("should reject when the store fails all requests", function()
338
+ local mock = DataStoreMock.new()
339
+ mock:FailAllRequests()
340
+
341
+ local promise = DataStorePromises.removeAsync(mock, "key")
342
+ if not PromiseTestUtils.awaitSettled(promise) then
343
+ expect("promise hung").toEqual("promise settled")
344
+ return
345
+ end
346
+
347
+ local ok = promise:Yield()
348
+ expect(ok).toEqual(false)
349
+ end)
350
+
351
+ it("should throw synchronously for a bad robloxDataStore", function()
352
+ expect(function()
353
+ DataStorePromises.removeAsync({} :: any, "key")
354
+ end).toThrow("Bad robloxDataStore")
355
+ end)
356
+
357
+ it("should throw synchronously for a non-string key", function()
358
+ local mock = DataStoreMock.new()
359
+ expect(function()
360
+ DataStorePromises.removeAsync(mock, 5 :: any)
361
+ end).toThrow("Bad key")
362
+ end)
363
+ end)
@@ -1,3 +1,6 @@
1
1
  return {
2
2
  testMatch = { "**/*.spec" },
3
+ -- A modest bump over the 5s default: enough to absorb MessagingService loopback variance in the
4
+ -- two-server tests, without masking the hang tests (they fail via a bounded assertion at ~5s).
5
+ testTimeout = 10000,
3
6
  }