@quenty/datastore 13.51.1 → 13.52.1
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 +19 -0
- package/package.json +17 -16
- package/src/Client/Cmdr/DataStoreCmdrServiceClient.lua +48 -0
- package/src/Client/DataStoreServiceClient.lua +35 -0
- package/src/Server/Cmdr/DataStoreCmdrService.lua +454 -0
- package/src/Server/Cmdr/DataStoreCmdrService.spec.lua +370 -0
- package/src/Server/DataStore.lua +21 -0
- package/src/Server/DataStoreLockHelper.lua +8 -86
- package/src/Server/DataStoreService.lua +40 -0
- package/src/Server/Modules/DataStoreLockUtils.lua +139 -0
- package/src/Server/PlayerDataStoreHandle.lua +103 -0
- package/src/Server/PlayerDataStoreManager.Handles.spec.lua +136 -0
- package/src/Server/PlayerDataStoreManager.SessionLockTools.spec.lua +228 -0
- package/src/Server/PlayerDataStoreManager.lua +282 -0
- package/src/Server/PlayerDataStoreManager.spec.lua +86 -0
- package/src/Server/PlayerDataStoreService.lua +112 -0
- package/src/Server/PlayerDataStoreService.spec.lua +65 -0
- package/src/Shared/Cmdr/DataStoreCmdrUtils.lua +57 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
Pure reading and writing of the session-lock envelope a [DataStore] keeps under the `lock` field.
|
|
4
|
+
|
|
5
|
+
[DataStoreLockHelper] owns the live path, where the lock is only ever touched by the session that
|
|
6
|
+
holds it. Tooling needs the other path: inspect or clear the lock on a key belonging to a player
|
|
7
|
+
who is not in this server, through a raw datastore write with no session at all. Both go through
|
|
8
|
+
here so there is one definition of the envelope.
|
|
9
|
+
|
|
10
|
+
@server
|
|
11
|
+
@class DataStoreLockUtils
|
|
12
|
+
]=]
|
|
13
|
+
|
|
14
|
+
local DataStoreLockUtils = {}
|
|
15
|
+
|
|
16
|
+
export type LockedSessionData = {
|
|
17
|
+
SessionId: string,
|
|
18
|
+
PlaceId: number,
|
|
19
|
+
JobId: string,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type LockData = {
|
|
23
|
+
LastUpdateTime: number?,
|
|
24
|
+
ActiveSession: LockedSessionData?,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
--[=[
|
|
28
|
+
Reads session data back out of whatever the datastore returned, rejecting anything malformed.
|
|
29
|
+
|
|
30
|
+
@param sessionData any
|
|
31
|
+
@return LockedSessionData?
|
|
32
|
+
]=]
|
|
33
|
+
function DataStoreLockUtils.deserializeSessionData(sessionData: any): LockedSessionData?
|
|
34
|
+
if type(sessionData) ~= "table" then
|
|
35
|
+
return nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if type(sessionData.SessionId) ~= "string" then
|
|
39
|
+
return nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
if type(sessionData.PlaceId) ~= "number" then
|
|
43
|
+
return nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if type(sessionData.JobId) ~= "string" then
|
|
47
|
+
return nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
SessionId = sessionData.SessionId,
|
|
52
|
+
PlaceId = sessionData.PlaceId,
|
|
53
|
+
JobId = sessionData.JobId,
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
--[=[
|
|
58
|
+
Reads a lock envelope back out of whatever the datastore returned.
|
|
59
|
+
|
|
60
|
+
@param lockData any
|
|
61
|
+
@return LockData?
|
|
62
|
+
]=]
|
|
63
|
+
function DataStoreLockUtils.deserializeLockData(lockData: any): LockData?
|
|
64
|
+
if type(lockData) ~= "table" then
|
|
65
|
+
return nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
LastUpdateTime = if type(lockData.LastUpdateTime) == "number" then lockData.LastUpdateTime else nil,
|
|
70
|
+
ActiveSession = DataStoreLockUtils.deserializeSessionData(lockData.ActiveSession),
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
--[=[
|
|
75
|
+
Reads the lock off a whole stored profile.
|
|
76
|
+
|
|
77
|
+
@param data any -- the raw value stored at the key
|
|
78
|
+
@return LockData?
|
|
79
|
+
]=]
|
|
80
|
+
function DataStoreLockUtils.readLock(data: any): LockData?
|
|
81
|
+
if type(data) ~= "table" then
|
|
82
|
+
return nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
return DataStoreLockUtils.deserializeLockData(data.lock)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
--[=[
|
|
89
|
+
Builds the envelope a session writes to claim the key.
|
|
90
|
+
|
|
91
|
+
@param sessionData LockedSessionData
|
|
92
|
+
@param lastUpdateTime number? -- defaults to now
|
|
93
|
+
@return LockData
|
|
94
|
+
]=]
|
|
95
|
+
function DataStoreLockUtils.createLockData(sessionData: LockedSessionData, lastUpdateTime: number?): LockData
|
|
96
|
+
return {
|
|
97
|
+
LastUpdateTime = lastUpdateTime or os.time(),
|
|
98
|
+
ActiveSession = sessionData,
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
--[=[
|
|
103
|
+
Returns a copy of the profile with the lock set to `lockData`, or cleared when it is nil.
|
|
104
|
+
|
|
105
|
+
@param data any -- the raw value stored at the key
|
|
106
|
+
@param lockData LockData?
|
|
107
|
+
@return any
|
|
108
|
+
]=]
|
|
109
|
+
function DataStoreLockUtils.withLock(data: any, lockData: LockData?): any
|
|
110
|
+
if data == nil then
|
|
111
|
+
return if lockData == nil then {} else { lock = lockData }
|
|
112
|
+
elseif type(data) ~= "table" then
|
|
113
|
+
warn("[DataStoreLockUtils] - Data session locking is not available for non-table entries")
|
|
114
|
+
return data
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
local copy = table.clone(data)
|
|
118
|
+
copy.lock = lockData
|
|
119
|
+
return copy
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
--[=[
|
|
123
|
+
Renders a lock for a human reading command output.
|
|
124
|
+
|
|
125
|
+
@param lockData LockData?
|
|
126
|
+
@return string
|
|
127
|
+
]=]
|
|
128
|
+
function DataStoreLockUtils.toHumanReadable(lockData: LockData?): string
|
|
129
|
+
if lockData == nil or lockData.ActiveSession == nil then
|
|
130
|
+
return "unlocked"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
local session = lockData.ActiveSession
|
|
134
|
+
local age = if lockData.LastUpdateTime then `{os.time() - lockData.LastUpdateTime}s ago` else "unknown age"
|
|
135
|
+
|
|
136
|
+
return `locked by PlaceId {session.PlaceId}, JobId {session.JobId}, SessionId {session.SessionId} (updated {age})`
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
return DataStoreLockUtils
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
A borrowed [DataStore] for a player, and the obligation to put it back.
|
|
4
|
+
|
|
5
|
+
Opening a store for a player who is not in this server takes their session lock, which kicks them
|
|
6
|
+
from wherever they were playing. Until that lock is dropped again they cannot rejoin, so releasing
|
|
7
|
+
it is the part that matters -- and a release the caller has to remember to perform is one that
|
|
8
|
+
eventually gets missed on an error path.
|
|
9
|
+
|
|
10
|
+
Handing back a handle makes it a [Maid]-shaped obligation instead: give it to a maid, or destroy
|
|
11
|
+
it, and the session is released once nothing is using it. Destroying twice is safe.
|
|
12
|
+
|
|
13
|
+
Handles are counted, so three systems loading the same player each get their own and the session
|
|
14
|
+
survives until the last one is destroyed.
|
|
15
|
+
|
|
16
|
+
A player in this server is not represented by a handle -- their session is owned by the join and
|
|
17
|
+
leave path, which reaches removal from several directions a handle could not model safely. So
|
|
18
|
+
destroying a handle never closes a live player's session; it only releases one this tooling
|
|
19
|
+
opened on behalf of someone absent.
|
|
20
|
+
|
|
21
|
+
```lua
|
|
22
|
+
local handle = manager:PromiseDataStoreHandle(userId):Yield()
|
|
23
|
+
local data = handle:GetDataStore():LoadAll({}):Yield()
|
|
24
|
+
handle:Destroy()
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
@server
|
|
28
|
+
@class PlayerDataStoreHandle
|
|
29
|
+
]=]
|
|
30
|
+
|
|
31
|
+
local require = require(script.Parent.loader).load(script)
|
|
32
|
+
|
|
33
|
+
local DataStore = require("DataStore")
|
|
34
|
+
|
|
35
|
+
local PlayerDataStoreHandle = {}
|
|
36
|
+
PlayerDataStoreHandle.ClassName = "PlayerDataStoreHandle"
|
|
37
|
+
PlayerDataStoreHandle.__index = PlayerDataStoreHandle
|
|
38
|
+
|
|
39
|
+
export type PlayerDataStoreHandle = typeof(setmetatable(
|
|
40
|
+
{} :: {
|
|
41
|
+
_dataStore: DataStore.DataStore?,
|
|
42
|
+
_release: (() -> ())?,
|
|
43
|
+
},
|
|
44
|
+
{} :: typeof({ __index = PlayerDataStoreHandle })
|
|
45
|
+
))
|
|
46
|
+
|
|
47
|
+
--[=[
|
|
48
|
+
Constructs a new handle over a datastore.
|
|
49
|
+
|
|
50
|
+
@param dataStore DataStore
|
|
51
|
+
@param release (() -> ())? -- invoked on destroy, when this handle is the one that borrowed the store
|
|
52
|
+
@return PlayerDataStoreHandle
|
|
53
|
+
]=]
|
|
54
|
+
function PlayerDataStoreHandle.new(dataStore: DataStore.DataStore, release: (() -> ())?): PlayerDataStoreHandle
|
|
55
|
+
local self: PlayerDataStoreHandle = setmetatable({} :: any, PlayerDataStoreHandle)
|
|
56
|
+
|
|
57
|
+
self._dataStore = assert(dataStore, "No dataStore")
|
|
58
|
+
self._release = release
|
|
59
|
+
|
|
60
|
+
return self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
--[=[
|
|
64
|
+
Returns whether the value is a handle.
|
|
65
|
+
|
|
66
|
+
@param value any
|
|
67
|
+
@return boolean
|
|
68
|
+
]=]
|
|
69
|
+
function PlayerDataStoreHandle.isPlayerDataStoreHandle(value: any): boolean
|
|
70
|
+
return type(value) == "table" and getmetatable(value) == PlayerDataStoreHandle
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
--[=[
|
|
74
|
+
Returns the datastore this handle holds. Errors once destroyed, since the session behind it may
|
|
75
|
+
already be closed.
|
|
76
|
+
|
|
77
|
+
@return DataStore
|
|
78
|
+
]=]
|
|
79
|
+
function PlayerDataStoreHandle.GetDataStore(self: PlayerDataStoreHandle): DataStore.DataStore
|
|
80
|
+
-- Bound to one value rather than returned straight out of the assert, which would hand back its
|
|
81
|
+
-- message as a second return and quietly widen every call site into a multiple-value expression.
|
|
82
|
+
local dataStore = self._dataStore
|
|
83
|
+
assert(dataStore, "Handle is destroyed")
|
|
84
|
+
|
|
85
|
+
return dataStore
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
--[=[
|
|
89
|
+
Drops this handle's reference to the session. The session itself is released once no handle and
|
|
90
|
+
no player in this server is still holding it.
|
|
91
|
+
]=]
|
|
92
|
+
function PlayerDataStoreHandle.Destroy(self: PlayerDataStoreHandle): ()
|
|
93
|
+
local release = self._release
|
|
94
|
+
|
|
95
|
+
self._dataStore = nil
|
|
96
|
+
self._release = nil
|
|
97
|
+
|
|
98
|
+
if release then
|
|
99
|
+
release()
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
return PlayerDataStoreHandle
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Counted datastore handles. Opening a store for an absent player takes their session lock, so the
|
|
4
|
+
thing worth pinning down is that the session is released once nothing is holding it -- and not
|
|
5
|
+
before, while another holder is still using it.
|
|
6
|
+
|
|
7
|
+
@class PlayerDataStoreManager.Handles.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
|
+
-- Returns the resolved value, or fails the spec and returns nil.
|
|
20
|
+
local function awaitValue(promise, label: string): any
|
|
21
|
+
if not PromiseTestUtils.awaitSettled(promise, 10) then
|
|
22
|
+
expect(`{label} hung`).toEqual(`{label} settled`)
|
|
23
|
+
return nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
local ok, value = promise:Yield()
|
|
27
|
+
expect(ok).toEqual(true)
|
|
28
|
+
return value
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
-- Drains the removals a test left in flight, so none of them settle during teardown. Note that this
|
|
32
|
+
-- also *removes* every remaining store, so it belongs at the end of a test and nowhere else.
|
|
33
|
+
local function settleSaves(controller): ()
|
|
34
|
+
if not PromiseTestUtils.awaitSettled(controller.manager:PromiseAllSaves(), 10) then
|
|
35
|
+
expect("saves hung").toEqual("saves settled")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe("PlayerDataStoreManager.PromiseDataStoreHandle", function()
|
|
40
|
+
it("hands back the store for the player", function()
|
|
41
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
42
|
+
|
|
43
|
+
controller.mock:SetRaw("user_1", { coins = 5 })
|
|
44
|
+
|
|
45
|
+
local handle = awaitValue(controller.manager:PromiseDataStoreHandle(1), "handle")
|
|
46
|
+
expect(handle).never.toBeNil()
|
|
47
|
+
expect(handle:GetDataStore()).never.toBeNil()
|
|
48
|
+
|
|
49
|
+
handle:Destroy()
|
|
50
|
+
settleSaves(controller)
|
|
51
|
+
|
|
52
|
+
controller:destroy()
|
|
53
|
+
end)
|
|
54
|
+
|
|
55
|
+
-- Whether the session is still open is read through store identity rather than the stored lock:
|
|
56
|
+
-- the lock is written asynchronously as the load settles, so asserting on it here would be timing
|
|
57
|
+
-- dependent. A released session is gone, so the next handle has to build a new store object.
|
|
58
|
+
it("releases the session once the handle is destroyed", function()
|
|
59
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
60
|
+
|
|
61
|
+
controller.mock:SetRaw("user_1", { coins = 5 })
|
|
62
|
+
|
|
63
|
+
local first = awaitValue(controller.manager:PromiseDataStoreHandle(1), "first")
|
|
64
|
+
local released = first:GetDataStore()
|
|
65
|
+
|
|
66
|
+
first:Destroy()
|
|
67
|
+
|
|
68
|
+
local second = awaitValue(controller.manager:PromiseDataStoreHandle(1), "second")
|
|
69
|
+
expect(second:GetDataStore() == released).toEqual(false)
|
|
70
|
+
|
|
71
|
+
second:Destroy()
|
|
72
|
+
settleSaves(controller)
|
|
73
|
+
|
|
74
|
+
controller:destroy()
|
|
75
|
+
end)
|
|
76
|
+
|
|
77
|
+
it("holds the session while another handle is still open", function()
|
|
78
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
79
|
+
|
|
80
|
+
controller.mock:SetRaw("user_1", { coins = 5 })
|
|
81
|
+
|
|
82
|
+
local first = awaitValue(controller.manager:PromiseDataStoreHandle(1), "first")
|
|
83
|
+
local second = awaitValue(controller.manager:PromiseDataStoreHandle(1), "second")
|
|
84
|
+
|
|
85
|
+
local shared = first:GetDataStore()
|
|
86
|
+
|
|
87
|
+
-- Both name the same session rather than opening a second one.
|
|
88
|
+
expect(second:GetDataStore() == shared).toEqual(true)
|
|
89
|
+
|
|
90
|
+
first:Destroy()
|
|
91
|
+
|
|
92
|
+
-- Still the same store, because the second handle never let go of it.
|
|
93
|
+
local third = awaitValue(controller.manager:PromiseDataStoreHandle(1), "third")
|
|
94
|
+
expect(third:GetDataStore() == shared).toEqual(true)
|
|
95
|
+
|
|
96
|
+
second:Destroy()
|
|
97
|
+
third:Destroy()
|
|
98
|
+
|
|
99
|
+
local fourth = awaitValue(controller.manager:PromiseDataStoreHandle(1), "fourth")
|
|
100
|
+
expect(fourth:GetDataStore() == shared).toEqual(false)
|
|
101
|
+
|
|
102
|
+
fourth:Destroy()
|
|
103
|
+
settleSaves(controller)
|
|
104
|
+
|
|
105
|
+
controller:destroy()
|
|
106
|
+
end)
|
|
107
|
+
|
|
108
|
+
it("survives being destroyed twice", function()
|
|
109
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
110
|
+
|
|
111
|
+
controller.mock:SetRaw("user_1", { coins = 5 })
|
|
112
|
+
|
|
113
|
+
local handle = awaitValue(controller.manager:PromiseDataStoreHandle(1), "handle")
|
|
114
|
+
handle:Destroy()
|
|
115
|
+
handle:Destroy()
|
|
116
|
+
settleSaves(controller)
|
|
117
|
+
|
|
118
|
+
controller:destroy()
|
|
119
|
+
end)
|
|
120
|
+
|
|
121
|
+
it("refuses to hand back a store once destroyed", function()
|
|
122
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
123
|
+
|
|
124
|
+
controller.mock:SetRaw("user_1", { coins = 5 })
|
|
125
|
+
|
|
126
|
+
local handle = awaitValue(controller.manager:PromiseDataStoreHandle(1), "handle")
|
|
127
|
+
handle:Destroy()
|
|
128
|
+
settleSaves(controller)
|
|
129
|
+
|
|
130
|
+
expect(function()
|
|
131
|
+
handle:GetDataStore()
|
|
132
|
+
end).toThrow()
|
|
133
|
+
|
|
134
|
+
controller:destroy()
|
|
135
|
+
end)
|
|
136
|
+
end)
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
The raw session-lock path: reading and writing the lock on a key without opening a session on it.
|
|
4
|
+
Usually the target is a player who is not in this server, but the write is deliberately permitted
|
|
5
|
+
against a live local session too -- see the last describe block.
|
|
6
|
+
|
|
7
|
+
@class PlayerDataStoreManager.SessionLockTools.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
|
+
local FOREIGN_SESSION = {
|
|
20
|
+
SessionId = "foreign-session",
|
|
21
|
+
PlaceId = 123,
|
|
22
|
+
JobId = "foreign-job",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
local function seedForeignLock(mock, key: string, profile: { [string]: any }?)
|
|
26
|
+
local data: { [string]: any } = profile or {}
|
|
27
|
+
data.lock = {
|
|
28
|
+
LastUpdateTime = os.time(),
|
|
29
|
+
ActiveSession = FOREIGN_SESSION,
|
|
30
|
+
}
|
|
31
|
+
mock:SetRaw(key, data)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
-- Returns the resolved value, or fails the spec and returns nil.
|
|
35
|
+
local function awaitValue(promise, label: string): any
|
|
36
|
+
if not PromiseTestUtils.awaitSettled(promise, 10) then
|
|
37
|
+
expect(`{label} hung`).toEqual(`{label} settled`)
|
|
38
|
+
return nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
local ok, value = promise:Yield()
|
|
42
|
+
expect(ok).toEqual(true)
|
|
43
|
+
return value
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
describe("PlayerDataStoreManager.PromiseReadSessionLock", function()
|
|
47
|
+
it("resolves nil for a key that was never written", function()
|
|
48
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
49
|
+
|
|
50
|
+
local lock = awaitValue(controller.manager:PromiseReadSessionLock(1), "read")
|
|
51
|
+
expect(lock).toBeNil()
|
|
52
|
+
|
|
53
|
+
controller:destroy()
|
|
54
|
+
end)
|
|
55
|
+
|
|
56
|
+
it("reports the session holding a foreign lock", function()
|
|
57
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
58
|
+
|
|
59
|
+
seedForeignLock(controller.mock, "user_1", { coins = 5 })
|
|
60
|
+
|
|
61
|
+
local lock = awaitValue(controller.manager:PromiseReadSessionLock(1), "read")
|
|
62
|
+
expect(lock).never.toBeNil()
|
|
63
|
+
expect(lock.ActiveSession).toEqual(FOREIGN_SESSION)
|
|
64
|
+
|
|
65
|
+
controller:destroy()
|
|
66
|
+
end)
|
|
67
|
+
|
|
68
|
+
it("resolves nil for a stored profile with no lock", function()
|
|
69
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
70
|
+
|
|
71
|
+
controller.mock:SetRaw("user_1", { coins = 5 })
|
|
72
|
+
|
|
73
|
+
local lock = awaitValue(controller.manager:PromiseReadSessionLock(1), "read")
|
|
74
|
+
expect(lock).toBeNil()
|
|
75
|
+
|
|
76
|
+
controller:destroy()
|
|
77
|
+
end)
|
|
78
|
+
end)
|
|
79
|
+
|
|
80
|
+
describe("PlayerDataStoreManager.PromiseUnlockSession", function()
|
|
81
|
+
it("clears a foreign lock and reports what it cleared", function()
|
|
82
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
83
|
+
|
|
84
|
+
seedForeignLock(controller.mock, "user_1", { coins = 5 })
|
|
85
|
+
|
|
86
|
+
local previous = awaitValue(controller.manager:PromiseUnlockSession(1), "unlock")
|
|
87
|
+
expect(previous).never.toBeNil()
|
|
88
|
+
expect(previous.ActiveSession).toEqual(FOREIGN_SESSION)
|
|
89
|
+
|
|
90
|
+
expect(controller.mock:GetRaw("user_1").lock).toBeNil()
|
|
91
|
+
|
|
92
|
+
controller:destroy()
|
|
93
|
+
end)
|
|
94
|
+
|
|
95
|
+
it("leaves the rest of the profile untouched", function()
|
|
96
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
97
|
+
|
|
98
|
+
seedForeignLock(controller.mock, "user_1", { coins = 5, level = 3 })
|
|
99
|
+
|
|
100
|
+
awaitValue(controller.manager:PromiseUnlockSession(1), "unlock")
|
|
101
|
+
|
|
102
|
+
local raw = controller.mock:GetRaw("user_1")
|
|
103
|
+
expect(raw.coins).toEqual(5)
|
|
104
|
+
expect(raw.level).toEqual(3)
|
|
105
|
+
|
|
106
|
+
controller:destroy()
|
|
107
|
+
end)
|
|
108
|
+
|
|
109
|
+
it("resolves nil on an already-unlocked key", function()
|
|
110
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
111
|
+
|
|
112
|
+
controller.mock:SetRaw("user_1", { coins = 5 })
|
|
113
|
+
|
|
114
|
+
local previous = awaitValue(controller.manager:PromiseUnlockSession(1), "unlock")
|
|
115
|
+
expect(previous).toBeNil()
|
|
116
|
+
|
|
117
|
+
controller:destroy()
|
|
118
|
+
end)
|
|
119
|
+
|
|
120
|
+
it("does not create an entry for a key that was never written", function()
|
|
121
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
122
|
+
|
|
123
|
+
awaitValue(controller.manager:PromiseUnlockSession(1), "unlock")
|
|
124
|
+
|
|
125
|
+
expect(controller.mock:GetRaw("user_1")).toBeNil()
|
|
126
|
+
|
|
127
|
+
controller:destroy()
|
|
128
|
+
end)
|
|
129
|
+
end)
|
|
130
|
+
|
|
131
|
+
describe("PlayerDataStoreManager.PromiseLockSession", function()
|
|
132
|
+
it("claims an unlocked key", function()
|
|
133
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
134
|
+
|
|
135
|
+
controller.mock:SetRaw("user_1", { coins = 5 })
|
|
136
|
+
|
|
137
|
+
local previous = awaitValue(controller.manager:PromiseLockSession(1), "lock")
|
|
138
|
+
expect(previous).toBeNil()
|
|
139
|
+
|
|
140
|
+
local lock = awaitValue(controller.manager:PromiseReadSessionLock(1), "read")
|
|
141
|
+
expect(lock).never.toBeNil()
|
|
142
|
+
expect(lock.ActiveSession.PlaceId).toEqual(game.PlaceId)
|
|
143
|
+
expect(controller.mock:GetRaw("user_1").coins).toEqual(5)
|
|
144
|
+
|
|
145
|
+
controller:destroy()
|
|
146
|
+
end)
|
|
147
|
+
|
|
148
|
+
it("reports the lock it replaced", function()
|
|
149
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
150
|
+
|
|
151
|
+
seedForeignLock(controller.mock, "user_1", { coins = 5 })
|
|
152
|
+
|
|
153
|
+
local previous = awaitValue(controller.manager:PromiseLockSession(1), "lock")
|
|
154
|
+
expect(previous).never.toBeNil()
|
|
155
|
+
expect(previous.ActiveSession).toEqual(FOREIGN_SESSION)
|
|
156
|
+
|
|
157
|
+
controller:destroy()
|
|
158
|
+
end)
|
|
159
|
+
|
|
160
|
+
it("claims a key that was never written", function()
|
|
161
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
162
|
+
|
|
163
|
+
awaitValue(controller.manager:PromiseLockSession(1), "lock")
|
|
164
|
+
|
|
165
|
+
local lock = awaitValue(controller.manager:PromiseReadSessionLock(1), "read")
|
|
166
|
+
expect(lock).never.toBeNil()
|
|
167
|
+
|
|
168
|
+
controller:destroy()
|
|
169
|
+
end)
|
|
170
|
+
end)
|
|
171
|
+
|
|
172
|
+
describe("PlayerDataStoreManager raw lock writes against a live session", function()
|
|
173
|
+
-- Allowed on purpose: pulling the key out from under a live local session is the failure the
|
|
174
|
+
-- lock/unlock debug tools exist to provoke. See PlayerDataStoreManager._promiseWriteRawSessionLock.
|
|
175
|
+
it("writes the lock while this server holds a session for that user", function()
|
|
176
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
177
|
+
|
|
178
|
+
if not controller.storeAndAwaitLock() then
|
|
179
|
+
expect("lock was never acquired").toEqual("lock was acquired")
|
|
180
|
+
controller:destroy()
|
|
181
|
+
return
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
local previous = awaitValue(controller.manager:PromiseUnlockSession(1), "unlock")
|
|
185
|
+
expect(previous).never.toBeNil()
|
|
186
|
+
expect(previous.ActiveSession.JobId).toEqual(game.JobId)
|
|
187
|
+
expect(controller.mock:GetRaw("user_1").lock).toBeNil()
|
|
188
|
+
|
|
189
|
+
controller:destroy()
|
|
190
|
+
end)
|
|
191
|
+
|
|
192
|
+
it("still serves a read while this server holds a session", function()
|
|
193
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
194
|
+
|
|
195
|
+
if not controller.storeAndAwaitLock() then
|
|
196
|
+
expect("lock was never acquired").toEqual("lock was acquired")
|
|
197
|
+
controller:destroy()
|
|
198
|
+
return
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
local lock = awaitValue(controller.manager:PromiseReadSessionLock(1), "read")
|
|
202
|
+
expect(lock).never.toBeNil()
|
|
203
|
+
expect(lock.ActiveSession.JobId).toEqual(game.JobId)
|
|
204
|
+
|
|
205
|
+
controller:destroy()
|
|
206
|
+
end)
|
|
207
|
+
|
|
208
|
+
it("writes once the session has been removed", function()
|
|
209
|
+
local controller = DataStoreTestUtils.setupDataStoreManager()
|
|
210
|
+
|
|
211
|
+
if not controller.storeAndAwaitLock() then
|
|
212
|
+
expect("lock was never acquired").toEqual("lock was acquired")
|
|
213
|
+
controller:destroy()
|
|
214
|
+
return
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
if not PromiseTestUtils.awaitSettled(controller.promiseShutdown({ 1 }), 10) then
|
|
218
|
+
expect("shutdown hung").toEqual("shutdown settled")
|
|
219
|
+
controller:destroy()
|
|
220
|
+
return
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
awaitValue(controller.manager:PromiseLockSession(1), "lock")
|
|
224
|
+
expect(controller.mock:GetRaw("user_1").lock).never.toBeNil()
|
|
225
|
+
|
|
226
|
+
controller:destroy()
|
|
227
|
+
end)
|
|
228
|
+
end)
|