@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.
- package/CHANGELOG.md +4 -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,202 @@
|
|
|
1
|
+
--!nonstrict
|
|
2
|
+
--[[
|
|
3
|
+
Integration coverage for PlayerDataStoreManager against a mocked Roblox datastore. The manager
|
|
4
|
+
auto-enables session locking and session messaging on every DataStore it creates, so even a load
|
|
5
|
+
does an UpdateAsync round-trip through the mock. Tests use numeric userIds, never real Players.
|
|
6
|
+
|
|
7
|
+
@class PlayerDataStoreManager.spec.lua
|
|
8
|
+
]]
|
|
9
|
+
local require = require(script.Parent.loader).load(script)
|
|
10
|
+
|
|
11
|
+
local DataStoreTestUtils = require("DataStoreTestUtils")
|
|
12
|
+
local Jest = require("Jest")
|
|
13
|
+
local PromiseTestUtils = require("PromiseTestUtils")
|
|
14
|
+
|
|
15
|
+
local describe = Jest.Globals.describe
|
|
16
|
+
local expect = Jest.Globals.expect
|
|
17
|
+
local it = Jest.Globals.it
|
|
18
|
+
|
|
19
|
+
-- Asserts the promise settled within the timeout and returns whether it is now safe to :Yield(), so
|
|
20
|
+
-- a hung promise fails the test instead of freezing the runner.
|
|
21
|
+
local function expectSettled(promise, timeout: number?): boolean
|
|
22
|
+
local settled = PromiseTestUtils.awaitSettled(promise, timeout)
|
|
23
|
+
expect(settled).toEqual(true)
|
|
24
|
+
return settled
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
describe("PlayerDataStoreManager.GetDataStore", function()
|
|
28
|
+
it("should return a datastore for a fresh user", function()
|
|
29
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
30
|
+
|
|
31
|
+
local dataStore = controller.manager:GetDataStore(1)
|
|
32
|
+
expect(dataStore).never.toBeNil()
|
|
33
|
+
|
|
34
|
+
controller:destroy()
|
|
35
|
+
end)
|
|
36
|
+
|
|
37
|
+
it("should cache the datastore per user", function()
|
|
38
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
39
|
+
|
|
40
|
+
local first = controller.manager:GetDataStore(1)
|
|
41
|
+
local second = controller.manager:GetDataStore(1)
|
|
42
|
+
expect(first).toEqual(second)
|
|
43
|
+
|
|
44
|
+
local other = controller.manager:GetDataStore(2)
|
|
45
|
+
expect((first == other)).toEqual(false)
|
|
46
|
+
|
|
47
|
+
controller:destroy()
|
|
48
|
+
end)
|
|
49
|
+
|
|
50
|
+
it("should apply the key generator", function()
|
|
51
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
52
|
+
|
|
53
|
+
local dataStore = controller.manager:GetDataStore(1)
|
|
54
|
+
expect((dataStore:GetKey())).toEqual("user_1")
|
|
55
|
+
|
|
56
|
+
controller:destroy()
|
|
57
|
+
end)
|
|
58
|
+
end)
|
|
59
|
+
|
|
60
|
+
describe("PlayerDataStoreManager.PromiseDataStore", function()
|
|
61
|
+
it("should resolve the datastore and load successfully against a healthy mock", function()
|
|
62
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
63
|
+
|
|
64
|
+
local promise = controller.manager:PromiseDataStore(1)
|
|
65
|
+
if not expectSettled(promise, 10) then
|
|
66
|
+
controller:destroy()
|
|
67
|
+
return
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
local ok, dataStore = promise:Yield()
|
|
71
|
+
expect(ok).toEqual(true)
|
|
72
|
+
expect(dataStore).never.toBeNil()
|
|
73
|
+
|
|
74
|
+
local loadPromise = dataStore:PromiseLoadSuccessful()
|
|
75
|
+
if not expectSettled(loadPromise, 10) then
|
|
76
|
+
controller:destroy()
|
|
77
|
+
return
|
|
78
|
+
end
|
|
79
|
+
expect((loadPromise:Wait())).toEqual(true)
|
|
80
|
+
|
|
81
|
+
controller:destroy()
|
|
82
|
+
end)
|
|
83
|
+
end)
|
|
84
|
+
|
|
85
|
+
describe("PlayerDataStoreManager persistence", function()
|
|
86
|
+
it("should round-trip a stored value across a removal/reload", function()
|
|
87
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
88
|
+
|
|
89
|
+
local dataStore = controller.manager:GetDataStore(1)
|
|
90
|
+
dataStore:Store("coins", 5)
|
|
91
|
+
|
|
92
|
+
-- Removal saves (SaveAndCloseSession) then closes the session, asynchronously.
|
|
93
|
+
controller.manager:RemovePlayerDataStore(1)
|
|
94
|
+
|
|
95
|
+
-- PromiseDataStore waits for the in-progress removal to finish before handing back a
|
|
96
|
+
-- fresh datastore for the same user.
|
|
97
|
+
local promise = controller.manager:PromiseDataStore(1)
|
|
98
|
+
if not expectSettled(promise, 10) then
|
|
99
|
+
controller:destroy()
|
|
100
|
+
return
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
local ok, reloaded = promise:Yield()
|
|
104
|
+
expect(ok).toEqual(true)
|
|
105
|
+
|
|
106
|
+
local loadPromise = reloaded:Load("coins")
|
|
107
|
+
if not expectSettled(loadPromise, 10) then
|
|
108
|
+
controller:destroy()
|
|
109
|
+
return
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
local loadOk, value = loadPromise:Yield()
|
|
113
|
+
expect(loadOk).toEqual(true)
|
|
114
|
+
expect(value).toEqual(5)
|
|
115
|
+
|
|
116
|
+
controller:destroy()
|
|
117
|
+
end)
|
|
118
|
+
end)
|
|
119
|
+
|
|
120
|
+
describe("PlayerDataStoreManager.AddRemovingCallback", function()
|
|
121
|
+
it("should invoke the removing callback when a user's datastore is removed", function()
|
|
122
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
123
|
+
|
|
124
|
+
local ran = false
|
|
125
|
+
controller.manager:AddRemovingCallback(function()
|
|
126
|
+
ran = true
|
|
127
|
+
end)
|
|
128
|
+
|
|
129
|
+
controller.manager:GetDataStore(1)
|
|
130
|
+
|
|
131
|
+
-- Drain the removal to be sure the callback fired.
|
|
132
|
+
local promise = controller.manager:PromiseAllSaves()
|
|
133
|
+
if not expectSettled(promise, 10) then
|
|
134
|
+
controller:destroy()
|
|
135
|
+
return
|
|
136
|
+
end
|
|
137
|
+
expect((promise:Yield())).toEqual(true)
|
|
138
|
+
|
|
139
|
+
expect(ran).toEqual(true)
|
|
140
|
+
|
|
141
|
+
controller:destroy()
|
|
142
|
+
end)
|
|
143
|
+
end)
|
|
144
|
+
|
|
145
|
+
describe("PlayerDataStoreManager.PromiseAllSaves", function()
|
|
146
|
+
it("should resolve after removing all datastores and flushing pending saves", function()
|
|
147
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
148
|
+
|
|
149
|
+
controller.manager:GetDataStore(1):Store("coins", 1)
|
|
150
|
+
controller.manager:GetDataStore(2):Store("coins", 2)
|
|
151
|
+
|
|
152
|
+
local promise = controller.manager:PromiseAllSaves()
|
|
153
|
+
if not expectSettled(promise, 10) then
|
|
154
|
+
controller:destroy()
|
|
155
|
+
return
|
|
156
|
+
end
|
|
157
|
+
expect((promise:Yield())).toEqual(true)
|
|
158
|
+
|
|
159
|
+
controller:destroy()
|
|
160
|
+
end)
|
|
161
|
+
end)
|
|
162
|
+
|
|
163
|
+
describe("PlayerDataStoreManager teardown", function()
|
|
164
|
+
it("destroys the datastores it still owns when the manager is destroyed", function()
|
|
165
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
166
|
+
|
|
167
|
+
local dataStore = controller.manager:GetDataStore(1)
|
|
168
|
+
if not expectSettled(dataStore:PromiseLoadSuccessful(), 10) then
|
|
169
|
+
controller:destroy()
|
|
170
|
+
return
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
controller.manager:Destroy()
|
|
174
|
+
|
|
175
|
+
-- BaseObject.Destroy clears the metatable, so a torn-down store reads a nil metatable.
|
|
176
|
+
expect(getmetatable(dataStore)).toBeNil()
|
|
177
|
+
|
|
178
|
+
controller:destroy()
|
|
179
|
+
end)
|
|
180
|
+
|
|
181
|
+
it("flushes staged data synchronously to the underlying store when destroyed", function()
|
|
182
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
183
|
+
|
|
184
|
+
local dataStore = controller.manager:GetDataStore(1)
|
|
185
|
+
if not expectSettled(dataStore:PromiseLoadSuccessful(), 10) then
|
|
186
|
+
controller:destroy()
|
|
187
|
+
return
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
dataStore:Store("coins", 5)
|
|
191
|
+
|
|
192
|
+
-- Destroy must fire a synchronous Save before tearing each store down, so the staged value is
|
|
193
|
+
-- already persisted the instant Destroy() returns -- asserted with no awaiting.
|
|
194
|
+
controller.manager:Destroy()
|
|
195
|
+
|
|
196
|
+
local raw = controller.mock:GetRaw("user_1")
|
|
197
|
+
expect(raw).never.toBeNil()
|
|
198
|
+
expect(raw.coins).toEqual(5)
|
|
199
|
+
|
|
200
|
+
controller:destroy()
|
|
201
|
+
end)
|
|
202
|
+
end)
|
|
@@ -27,6 +27,7 @@ export type PlayerDataStoreService = typeof(setmetatable(
|
|
|
27
27
|
_dataStoreManagerPromise: Promise.Promise<PlayerDataStoreManager.PlayerDataStoreManager>,
|
|
28
28
|
_bindToCloseService: any,
|
|
29
29
|
_promiseStarted: Promise.Promise<()>,
|
|
30
|
+
_robloxDataStoreOverride: any?,
|
|
30
31
|
},
|
|
31
32
|
{} :: typeof({ __index = PlayerDataStoreService })
|
|
32
33
|
))
|
|
@@ -91,6 +92,20 @@ function PlayerDataStoreService.SetDataStoreScope(self: PlayerDataStoreService,
|
|
|
91
92
|
self._dataStoreScope = dataStoreScope
|
|
92
93
|
end
|
|
93
94
|
|
|
95
|
+
--[=[
|
|
96
|
+
Injects the underlying datastore the manager wraps, instead of resolving a real one. Accepts
|
|
97
|
+
a real datastore or a [DataStoreMock]. Intended for testing; must be called before the manager
|
|
98
|
+
is first built.
|
|
99
|
+
|
|
100
|
+
@param robloxDataStore DataStore | DataStoreMock
|
|
101
|
+
]=]
|
|
102
|
+
function PlayerDataStoreService.SetRobloxDataStore(self: PlayerDataStoreService, robloxDataStore: any): ()
|
|
103
|
+
assert(DataStorePromises.isDataStore(robloxDataStore), "Bad robloxDataStore")
|
|
104
|
+
assert(not self._dataStoreManagerPromise, "Already built manager, cannot override")
|
|
105
|
+
|
|
106
|
+
self._robloxDataStoreOverride = robloxDataStore
|
|
107
|
+
end
|
|
108
|
+
|
|
94
109
|
--[=[
|
|
95
110
|
Gets the datastore for the player.
|
|
96
111
|
|
|
@@ -138,6 +153,9 @@ function PlayerDataStoreService.PromiseManager(
|
|
|
138
153
|
|
|
139
154
|
self._dataStoreManagerPromise = self._promiseStarted
|
|
140
155
|
:Then(function()
|
|
156
|
+
if self._robloxDataStoreOverride then
|
|
157
|
+
return self._robloxDataStoreOverride
|
|
158
|
+
end
|
|
141
159
|
return DataStorePromises.promiseDataStore(self._dataStoreName, self._dataStoreScope)
|
|
142
160
|
end)
|
|
143
161
|
:Then(function(dataStore)
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
--!nonstrict
|
|
2
|
+
--[[
|
|
3
|
+
Integration coverage for PlayerDataStoreService, the ServiceBag-driven wrapper around one
|
|
4
|
+
PlayerDataStoreManager, with the underlying datastore injected via the SetRobloxDataStore test
|
|
5
|
+
seam. The manager auto-enables session locking, so even a load does an UpdateAsync round-trip
|
|
6
|
+
through the mock. Tests use numeric userIds, never real Players.
|
|
7
|
+
|
|
8
|
+
@class PlayerDataStoreService.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
|
+
|
|
25
|
+
local serviceBag = maid:Add(ServiceBag.new())
|
|
26
|
+
local service = serviceBag:GetService(require("PlayerDataStoreService"))
|
|
27
|
+
serviceBag:Init()
|
|
28
|
+
|
|
29
|
+
if mock then
|
|
30
|
+
service:SetRobloxDataStore(mock)
|
|
31
|
+
serviceBag:Start()
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
service = service,
|
|
36
|
+
mock = mock,
|
|
37
|
+
destroy = function()
|
|
38
|
+
maid:DoCleaning()
|
|
39
|
+
end,
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe("PlayerDataStoreService.PromiseDataStore", function()
|
|
44
|
+
it("should resolve a datastore and load successfully against a healthy mock", function()
|
|
45
|
+
local controller = setup(DataStoreMock.new())
|
|
46
|
+
|
|
47
|
+
local promise = controller.service:PromiseDataStore(1)
|
|
48
|
+
if not PromiseTestUtils.awaitSettled(promise, 10) then
|
|
49
|
+
expect("hung").toEqual("settled")
|
|
50
|
+
controller:destroy()
|
|
51
|
+
return
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
local ok, dataStore = promise:Yield()
|
|
55
|
+
expect(ok).toEqual(true)
|
|
56
|
+
expect(dataStore).never.toBeNil()
|
|
57
|
+
|
|
58
|
+
local loadPromise = dataStore:PromiseLoadSuccessful()
|
|
59
|
+
if not PromiseTestUtils.awaitSettled(loadPromise, 10) then
|
|
60
|
+
expect("hung").toEqual("settled")
|
|
61
|
+
controller:destroy()
|
|
62
|
+
return
|
|
63
|
+
end
|
|
64
|
+
expect((loadPromise:Wait())).toEqual(true)
|
|
65
|
+
|
|
66
|
+
controller:destroy()
|
|
67
|
+
end)
|
|
68
|
+
end)
|
|
69
|
+
|
|
70
|
+
describe("PlayerDataStoreService configuration guards", function()
|
|
71
|
+
it("should throw when SetDataStoreName is called after start", function()
|
|
72
|
+
local controller = setup(DataStoreMock.new())
|
|
73
|
+
|
|
74
|
+
expect(function()
|
|
75
|
+
controller.service:SetDataStoreName("X")
|
|
76
|
+
end).toThrow("Already started, cannot configure")
|
|
77
|
+
|
|
78
|
+
controller:destroy()
|
|
79
|
+
end)
|
|
80
|
+
|
|
81
|
+
it("should throw when SetDataStoreScope is called after start", function()
|
|
82
|
+
local controller = setup(DataStoreMock.new())
|
|
83
|
+
|
|
84
|
+
expect(function()
|
|
85
|
+
controller.service:SetDataStoreScope("X")
|
|
86
|
+
end).toThrow("Already started, cannot configure")
|
|
87
|
+
|
|
88
|
+
controller:destroy()
|
|
89
|
+
end)
|
|
90
|
+
|
|
91
|
+
it("should throw when SetRobloxDataStore is called after the manager is built", function()
|
|
92
|
+
local controller = setup(DataStoreMock.new())
|
|
93
|
+
|
|
94
|
+
-- Building the manager (via PromiseDataStore) locks out further overrides.
|
|
95
|
+
local promise = controller.service:PromiseDataStore(1)
|
|
96
|
+
if not PromiseTestUtils.awaitSettled(promise, 10) then
|
|
97
|
+
expect("hung").toEqual("settled")
|
|
98
|
+
controller:destroy()
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
expect(function()
|
|
103
|
+
controller.service:SetRobloxDataStore(controller.mock)
|
|
104
|
+
end).toThrow("Already built manager")
|
|
105
|
+
|
|
106
|
+
controller:destroy()
|
|
107
|
+
end)
|
|
108
|
+
|
|
109
|
+
it("should throw when SetRobloxDataStore is given a bad datastore", function()
|
|
110
|
+
local controller = setup()
|
|
111
|
+
|
|
112
|
+
expect(function()
|
|
113
|
+
controller.service:SetRobloxDataStore(nil)
|
|
114
|
+
end).toThrow("Bad robloxDataStore")
|
|
115
|
+
|
|
116
|
+
expect(function()
|
|
117
|
+
controller.service:SetRobloxDataStore({})
|
|
118
|
+
end).toThrow("Bad robloxDataStore")
|
|
119
|
+
|
|
120
|
+
controller:destroy()
|
|
121
|
+
end)
|
|
122
|
+
end)
|
|
123
|
+
|
|
124
|
+
describe("PlayerDataStoreService.PromiseAddRemovingCallback", function()
|
|
125
|
+
it("should resolve after registering the removing callback", function()
|
|
126
|
+
local controller = setup(DataStoreMock.new())
|
|
127
|
+
|
|
128
|
+
local promise = controller.service:PromiseAddRemovingCallback(function() end)
|
|
129
|
+
if not PromiseTestUtils.awaitSettled(promise, 10) then
|
|
130
|
+
expect("hung").toEqual("settled")
|
|
131
|
+
controller:destroy()
|
|
132
|
+
return
|
|
133
|
+
end
|
|
134
|
+
expect((promise:Yield())).toEqual(true)
|
|
135
|
+
|
|
136
|
+
controller:destroy()
|
|
137
|
+
end)
|
|
138
|
+
end)
|
|
139
|
+
|
|
140
|
+
describe("PlayerDataStoreService failure handling", function()
|
|
141
|
+
it("surfaces a datastore failure to the player fast instead of hanging", function()
|
|
142
|
+
local mock = DataStoreMock.new()
|
|
143
|
+
mock:FailAllRequests()
|
|
144
|
+
|
|
145
|
+
local controller = setup(mock)
|
|
146
|
+
|
|
147
|
+
-- PromiseDataStore resolves synchronously; the session-locked load underneath is what must
|
|
148
|
+
-- settle rather than hang against a failing datastore.
|
|
149
|
+
local promise = controller.service:PromiseDataStore(1)
|
|
150
|
+
if not PromiseTestUtils.awaitSettled(promise, 10) then
|
|
151
|
+
expect("hung").toEqual("settled")
|
|
152
|
+
controller:destroy()
|
|
153
|
+
return
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
local ok, dataStore = promise:Yield()
|
|
157
|
+
expect(ok).toEqual(true)
|
|
158
|
+
expect(dataStore).never.toBeNil()
|
|
159
|
+
|
|
160
|
+
local loadPromise = dataStore:PromiseLoadSuccessful()
|
|
161
|
+
expect(PromiseTestUtils.awaitSettled(loadPromise, 5)).toEqual(true)
|
|
162
|
+
expect((loadPromise:Wait())).toEqual(false)
|
|
163
|
+
|
|
164
|
+
controller:destroy()
|
|
165
|
+
end)
|
|
166
|
+
end)
|
|
167
|
+
|
|
168
|
+
describe("PlayerDataStoreService teardown", function()
|
|
169
|
+
it("destroys the datastore its manager owns when the service is destroyed", function()
|
|
170
|
+
local controller = setup(DataStoreMock.new())
|
|
171
|
+
|
|
172
|
+
local promise = controller.service:PromiseDataStore(1)
|
|
173
|
+
if not PromiseTestUtils.awaitSettled(promise, 10) then
|
|
174
|
+
expect("hung").toEqual("settled")
|
|
175
|
+
controller:destroy()
|
|
176
|
+
return
|
|
177
|
+
end
|
|
178
|
+
local _ok, dataStore = promise:Yield()
|
|
179
|
+
|
|
180
|
+
if not PromiseTestUtils.awaitSettled(dataStore:PromiseLoadSuccessful(), 10) then
|
|
181
|
+
expect("load hung").toEqual("load settled")
|
|
182
|
+
controller:destroy()
|
|
183
|
+
return
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
controller:destroy()
|
|
187
|
+
|
|
188
|
+
expect(getmetatable(dataStore)).toBeNil()
|
|
189
|
+
end)
|
|
190
|
+
|
|
191
|
+
it("flushes staged data synchronously to the underlying store when destroyed", function()
|
|
192
|
+
local controller = setup(DataStoreMock.new())
|
|
193
|
+
|
|
194
|
+
local promise = controller.service:PromiseDataStore(1)
|
|
195
|
+
if not PromiseTestUtils.awaitSettled(promise, 10) then
|
|
196
|
+
expect("hung").toEqual("settled")
|
|
197
|
+
controller:destroy()
|
|
198
|
+
return
|
|
199
|
+
end
|
|
200
|
+
local _ok, dataStore = promise:Yield()
|
|
201
|
+
|
|
202
|
+
if not PromiseTestUtils.awaitSettled(dataStore:PromiseLoadSuccessful(), 10) then
|
|
203
|
+
expect("load hung").toEqual("load settled")
|
|
204
|
+
controller:destroy()
|
|
205
|
+
return
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
dataStore:Store("coins", 7)
|
|
209
|
+
|
|
210
|
+
-- Destroy must fire a synchronous Save before tearing the store down.
|
|
211
|
+
controller:destroy()
|
|
212
|
+
|
|
213
|
+
-- keyGenerator maps userId 1 -> "1"
|
|
214
|
+
local raw = controller.mock:GetRaw("1")
|
|
215
|
+
expect(raw).never.toBeNil()
|
|
216
|
+
expect(raw.coins).toEqual(7)
|
|
217
|
+
end)
|
|
218
|
+
end)
|
|
@@ -51,7 +51,7 @@ function PrivateServerDataStoreService.PromiseDataStore(
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
self._dataStorePromise = self:_promiseRobloxDataStore():Then(function(robloxDataStore)
|
|
54
|
-
local dataStore =
|
|
54
|
+
local dataStore = DataStore.new(robloxDataStore, self:_getKey())
|
|
55
55
|
|
|
56
56
|
if game.PrivateServerOwnerId ~= 0 then
|
|
57
57
|
dataStore:Store("LastPrivateServerOwnerId", game.PrivateServerOwnerId)
|
|
@@ -61,6 +61,14 @@ function PrivateServerDataStoreService.PromiseDataStore(
|
|
|
61
61
|
return dataStore:Save()
|
|
62
62
|
end))
|
|
63
63
|
|
|
64
|
+
-- On service teardown (hot reload / tests) flush and destroy the store. Save() is a best-effort
|
|
65
|
+
-- synchronous write before Destroy() cancels it.
|
|
66
|
+
self._maid:GiveTask(function()
|
|
67
|
+
-- Best-effort: swallow a rejection (e.g. the load failed) so it is not uncaught.
|
|
68
|
+
dataStore:Save():Catch(function() end)
|
|
69
|
+
dataStore:Destroy()
|
|
70
|
+
end)
|
|
71
|
+
|
|
64
72
|
return dataStore
|
|
65
73
|
end)
|
|
66
74
|
assert(self._dataStorePromise, "Typechecking assertion")
|
|
@@ -77,6 +85,20 @@ function PrivateServerDataStoreService.SetCustomKey(self: PrivateServerDataStore
|
|
|
77
85
|
self._customKey = customKey
|
|
78
86
|
end
|
|
79
87
|
|
|
88
|
+
--[=[
|
|
89
|
+
Injects the underlying datastore to use instead of resolving a real one. Accepts a real
|
|
90
|
+
datastore or a [DataStoreMock]. Intended for testing; must be called before the datastore
|
|
91
|
+
is first resolved.
|
|
92
|
+
|
|
93
|
+
@param robloxDataStore DataStore | DataStoreMock
|
|
94
|
+
]=]
|
|
95
|
+
function PrivateServerDataStoreService.SetRobloxDataStore(self: PrivateServerDataStoreService, robloxDataStore: any): ()
|
|
96
|
+
assert(DataStorePromises.isDataStore(robloxDataStore), "Bad robloxDataStore")
|
|
97
|
+
assert(not self._robloxDataStorePromise, "Already resolved robloxDataStore, cannot override")
|
|
98
|
+
|
|
99
|
+
self._robloxDataStorePromise = Promise.resolved(robloxDataStore)
|
|
100
|
+
end
|
|
101
|
+
|
|
80
102
|
function PrivateServerDataStoreService._promiseRobloxDataStore(
|
|
81
103
|
self: PrivateServerDataStoreService
|
|
82
104
|
): Promise.Promise<any>
|