@rbxts/dataforge 0.1.0 → 0.4.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/LICENSE +21 -0
- package/index.d.ts +318 -32
- package/package.json +7 -2
- package/src/errors.luau +211 -0
- package/src/hooks/memory.luau +1 -12
- package/src/hooks/roblox.luau +10 -1
- package/src/init.luau +20 -2
- package/src/mutex.luau +5 -5
- package/src/profile/common.luau +61 -12
- package/src/profile/lockless.luau +122 -51
- package/src/profile/peek.luau +30 -16
- package/src/profile/profile.luau +246 -116
- package/src/result.luau +61 -0
- package/src/store.luau +129 -44
- package/src/tx/handle.luau +82 -59
- package/src/tx/operation.luau +6 -3
- package/src/types.luau +236 -17
- package/src/utils.luau +37 -3
package/src/errors.luau
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
|
|
3
|
+
local types = require "./types"
|
|
4
|
+
|
|
5
|
+
--[[
|
|
6
|
+
Error tables. Every runtime failure the library reports is one of these:
|
|
7
|
+
a plain table with a `type`, a `message` and typed extra data, sharing one
|
|
8
|
+
metatable so `tostring(err)` yields the message and `errors.is` can tell
|
|
9
|
+
them apart from raw (string) errors thrown by user callbacks.
|
|
10
|
+
]]
|
|
11
|
+
|
|
12
|
+
type Array<T> = { T }
|
|
13
|
+
type Lock = types.Lock
|
|
14
|
+
|
|
15
|
+
local error_mt = {
|
|
16
|
+
__tostring = function(self: any): string
|
|
17
|
+
return self.message
|
|
18
|
+
end,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
local function make<T>(fields: T): T
|
|
22
|
+
return setmetatable(fields :: any, error_mt) :: any
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
local errors = {}
|
|
26
|
+
|
|
27
|
+
function errors.is(value: any): boolean
|
|
28
|
+
return type(value) == "table" and getmetatable(value) == error_mt
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
function errors.roblox(op: "get_async" | "update_async", store: string, key: string, cause: any): types.RobloxError
|
|
32
|
+
return make {
|
|
33
|
+
type = "roblox" :: "roblox",
|
|
34
|
+
message = `datastore: {op} on "{store}/{key}" failed after retries: {tostring(cause)}`,
|
|
35
|
+
op = op,
|
|
36
|
+
store = store,
|
|
37
|
+
key = key,
|
|
38
|
+
cause = cause,
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
function errors.timeout(key: string, lock: Lock?): types.TimeoutError
|
|
43
|
+
return make {
|
|
44
|
+
type = "timeout" :: "timeout",
|
|
45
|
+
message = `datastore: timed out loading "{key}" (session locked by another server)`,
|
|
46
|
+
key = key,
|
|
47
|
+
lock = lock,
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
function errors.lock_lost(profile: any, lock: Lock?): types.LockLostError
|
|
52
|
+
return make {
|
|
53
|
+
type = "lock_lost" :: "lock_lost",
|
|
54
|
+
message = `datastore: profile "{profile.key}" lost its session lock`,
|
|
55
|
+
profile = profile,
|
|
56
|
+
lock = lock,
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
function errors.released(profile: any): types.ReleasedError
|
|
61
|
+
return make {
|
|
62
|
+
type = "released" :: "released",
|
|
63
|
+
message = `datastore: profile "{profile.key}" is released (does not hold its session lock)`,
|
|
64
|
+
profile = profile,
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
function errors.lockless_locked(profile: any, lock: Lock): types.LocklessLockedError
|
|
69
|
+
return make {
|
|
70
|
+
type = "lockless_locked" :: "lockless_locked",
|
|
71
|
+
message = `datastore: lockless profile "{profile.key}" is session locked by another server`,
|
|
72
|
+
profile = profile,
|
|
73
|
+
lock = lock,
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
function errors.tx_lock_lost(profile: any, expected: Lock, lock: Lock?): types.TxLockLostError
|
|
78
|
+
return make {
|
|
79
|
+
type = "tx_lock_lost" :: "tx_lock_lost",
|
|
80
|
+
message = `datastore: transaction lock on "{profile.key}" was lost`,
|
|
81
|
+
profile = profile,
|
|
82
|
+
expected = expected,
|
|
83
|
+
lock = lock,
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
--- `profile` is the lockless / locked profile the call ran on, when there is
|
|
88
|
+
--- one (nil for `load` and `peek`).
|
|
89
|
+
function errors.outdated(
|
|
90
|
+
key: string,
|
|
91
|
+
record_migrations: Array<string>,
|
|
92
|
+
declared_migrations: Array<string>,
|
|
93
|
+
profile: any?
|
|
94
|
+
): types.OutdatedError
|
|
95
|
+
return make {
|
|
96
|
+
type = "outdated" :: "outdated",
|
|
97
|
+
message = `datastore: "{key}" was written by a newer server (record has {#record_migrations} applied migrations, this server declares {#declared_migrations})`,
|
|
98
|
+
key = key,
|
|
99
|
+
profile = profile,
|
|
100
|
+
record_migrations = record_migrations,
|
|
101
|
+
declared_migrations = declared_migrations,
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
function errors.predicate_failed(
|
|
106
|
+
key: string,
|
|
107
|
+
op: "load" | "save" | "fetch" | "peek",
|
|
108
|
+
data: any,
|
|
109
|
+
profile: any?
|
|
110
|
+
): types.PredicateFailedError
|
|
111
|
+
return make {
|
|
112
|
+
type = "predicate_failed" :: "predicate_failed",
|
|
113
|
+
message = `datastore: the store predicate rejected the stored data of "{key}" (on {op})`,
|
|
114
|
+
key = key,
|
|
115
|
+
op = op,
|
|
116
|
+
data = data,
|
|
117
|
+
profile = profile,
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
function errors.not_fetched(profile: any): types.NotFetchedError
|
|
122
|
+
return make {
|
|
123
|
+
type = "not_fetched" :: "not_fetched",
|
|
124
|
+
message = `datastore: lockless profile "{profile.key}" has not been fetched`,
|
|
125
|
+
profile = profile,
|
|
126
|
+
}
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
function errors.profile_closed(profile: any, reason: "unloaded" | "lock_lost"): types.ProfileClosedError
|
|
130
|
+
local message = if reason == "lock_lost"
|
|
131
|
+
then `datastore: profile "{profile.key}" is closed (lock lost)`
|
|
132
|
+
else `datastore: profile "{profile.key}" is closed`
|
|
133
|
+
return make {
|
|
134
|
+
type = "profile_closed" :: "profile_closed",
|
|
135
|
+
message = message,
|
|
136
|
+
profile = profile,
|
|
137
|
+
reason = reason,
|
|
138
|
+
}
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
--- The error a closed profile rejects calls with: the one its mutex was closed
|
|
142
|
+
--- with (carries the right reason), or a plain "unloaded" while `unload` is
|
|
143
|
+
--- still waiting for the mutex.
|
|
144
|
+
function errors.closed(profile: any): types.ProfileClosedError
|
|
145
|
+
local closed = profile._mutex._closed
|
|
146
|
+
if closed ~= nil then
|
|
147
|
+
return closed
|
|
148
|
+
end
|
|
149
|
+
return errors.profile_closed(profile, "unloaded")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
export type MigrationMismatchDetails = {
|
|
153
|
+
index: number,
|
|
154
|
+
expected: string,
|
|
155
|
+
actual: string,
|
|
156
|
+
applied: Array<string>,
|
|
157
|
+
declared: Array<string>,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function errors.migration_mismatch(key: string, details: MigrationMismatchDetails): types.MigrationMismatchError
|
|
161
|
+
local reason =
|
|
162
|
+
`migration mismatch at index {details.index}: record has "{details.actual}", declared is "{details.expected}"`
|
|
163
|
+
return make {
|
|
164
|
+
type = "migration_mismatch" :: "migration_mismatch",
|
|
165
|
+
message = `datastore: failed to apply migrations for "{key}": {reason}`,
|
|
166
|
+
key = key,
|
|
167
|
+
index = details.index,
|
|
168
|
+
expected = details.expected,
|
|
169
|
+
actual = details.actual,
|
|
170
|
+
applied = details.applied,
|
|
171
|
+
declared = details.declared,
|
|
172
|
+
}
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
function errors.tx_aborted(id: string, cause: types.Error?): types.TxAbortedError
|
|
176
|
+
local reason = if cause then (cause :: any).message else "marker resolved as aborted by another server"
|
|
177
|
+
return make {
|
|
178
|
+
type = "tx_aborted" :: "tx_aborted",
|
|
179
|
+
message = `datastore: transaction {id} aborted: {reason}`,
|
|
180
|
+
id = id,
|
|
181
|
+
cause = cause,
|
|
182
|
+
}
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
function errors.tx_marker_invalid(id: string, value: any): types.TxMarkerInvalidError
|
|
186
|
+
return make {
|
|
187
|
+
type = "tx_marker_invalid" :: "tx_marker_invalid",
|
|
188
|
+
message = `datastore: unexpected tx marker value for {id}: {tostring(value)}`,
|
|
189
|
+
id = id,
|
|
190
|
+
value = value,
|
|
191
|
+
}
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
function errors.store_closed(store: any): types.StoreClosedError
|
|
195
|
+
return make {
|
|
196
|
+
type = "store_closed" :: "store_closed",
|
|
197
|
+
message = `datastore: store "{store.config.name}" is closed`,
|
|
198
|
+
store = store,
|
|
199
|
+
}
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
function errors.already_loaded(store: any, key: string): types.AlreadyLoadedError
|
|
203
|
+
return make {
|
|
204
|
+
type = "already_loaded" :: "already_loaded",
|
|
205
|
+
message = `datastore: "{key}" is already loaded in this store`,
|
|
206
|
+
store = store,
|
|
207
|
+
key = key,
|
|
208
|
+
}
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
return errors
|
package/src/hooks/memory.luau
CHANGED
|
@@ -42,17 +42,6 @@ type Failure = {
|
|
|
42
42
|
message: string,
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
local HEX = "0123456789abcdef"
|
|
46
|
-
|
|
47
|
-
function random_jobid(): string
|
|
48
|
-
local chars = table.create(16)
|
|
49
|
-
for index = 1, 16 do
|
|
50
|
-
local nibble = math.random(1, 16)
|
|
51
|
-
chars[index] = string.sub(HEX, nibble, nibble)
|
|
52
|
-
end
|
|
53
|
-
return table.concat(chars)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
45
|
local memory = {}
|
|
57
46
|
|
|
58
47
|
function memory.create(scheduler: types.Scheduler): MemoryHook
|
|
@@ -113,7 +102,7 @@ function memory.create(scheduler: types.Scheduler): MemoryHook
|
|
|
113
102
|
end
|
|
114
103
|
|
|
115
104
|
-- one id per hook instance: locks are owned by job id, so it must be stable
|
|
116
|
-
local job_id =
|
|
105
|
+
local job_id = utils.uuid()
|
|
117
106
|
function self.job_id()
|
|
118
107
|
return job_id
|
|
119
108
|
end
|
package/src/hooks/roblox.luau
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
--!nolint UnknownGlobal
|
|
3
3
|
|
|
4
4
|
local types = require "../types"
|
|
5
|
+
local utils = require "../utils"
|
|
5
6
|
|
|
6
7
|
--[[
|
|
7
8
|
DataStoreService hook. Only usable inside Roblox.
|
|
@@ -17,8 +18,16 @@ function roblox_hook.create(): types.Hook
|
|
|
17
18
|
|
|
18
19
|
self.warn = warn
|
|
19
20
|
|
|
21
|
+
-- `game.JobId` is an empty string in Studio, so every Studio session would
|
|
22
|
+
-- share the same lock owner and transaction id prefix. Generate a random
|
|
23
|
+
-- one once per hook instead; locks are owned by job id, so it must be stable.
|
|
24
|
+
local job_id: string = game.JobId
|
|
25
|
+
if job_id == "" then
|
|
26
|
+
job_id = `studio-{utils.uuid()}`
|
|
27
|
+
end
|
|
28
|
+
|
|
20
29
|
function self.job_id()
|
|
21
|
-
return
|
|
30
|
+
return job_id
|
|
22
31
|
end
|
|
23
32
|
|
|
24
33
|
function self.get_datastore(name: string): types.DatastoreHandle
|
package/src/init.luau
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
local errors = require "@self/errors"
|
|
2
|
+
local result = require "@self/result"
|
|
1
3
|
local store = require "@self/store"
|
|
2
4
|
local tx = require "@self/tx/handle"
|
|
3
5
|
local types = require "@self/types"
|
|
@@ -18,6 +20,10 @@ export type Profile<T = any> = types.Profile<T>
|
|
|
18
20
|
export type LocklessProfile<T = any> = types.LocklessProfile<T>
|
|
19
21
|
export type ProfileBase<T = any> = types.ProfileBase<T>
|
|
20
22
|
export type PeekProfile<T = any> = types.PeekProfile<T>
|
|
23
|
+
export type Result<T> = types.Result<T>
|
|
24
|
+
export type Error = types.Error
|
|
25
|
+
export type Migration = types.Migration
|
|
26
|
+
export type DataDispatcher<T = any> = types.DataDispatcher<T>
|
|
21
27
|
|
|
22
28
|
local dataforge = {}
|
|
23
29
|
|
|
@@ -26,12 +32,14 @@ function dataforge.create_store<T>(config: Config<T>): Store<T>
|
|
|
26
32
|
end
|
|
27
33
|
|
|
28
34
|
--- Atomically updates any set of profiles, across stores and mixing locked and
|
|
29
|
-
--- lockless ones.
|
|
35
|
+
--- lockless ones. Ok(false) when `process` returns false, Ok(true) when
|
|
36
|
+
--- committed. Fails with `tx_aborted` (see its `cause`), `profile_closed`,
|
|
37
|
+
--- `released`, or what flushing a lockless participant raised.
|
|
30
38
|
function dataforge.transaction(
|
|
31
39
|
profiles: Array<ProfileBase>,
|
|
32
40
|
process: (ctx: TxContext) -> boolean?,
|
|
33
41
|
config: TransactionConfig?
|
|
34
|
-
): boolean
|
|
42
|
+
): Result<boolean>
|
|
35
43
|
local first = profiles[1]
|
|
36
44
|
local defaults: TransactionConfig? = if first
|
|
37
45
|
then {
|
|
@@ -45,6 +53,16 @@ function dataforge.transaction(
|
|
|
45
53
|
return tx.run(tx.resolve_config(config, defaults), profiles, process)
|
|
46
54
|
end
|
|
47
55
|
|
|
56
|
+
--- Whether `value` is a dataforge `Error` table (what a failed Result carries
|
|
57
|
+
--- and what the library throws internally). Lets a wrapper around library calls
|
|
58
|
+
--- tell a library error from any other thrown value.
|
|
59
|
+
dataforge.is_error = errors.is
|
|
60
|
+
|
|
61
|
+
--- The `Result` helpers (`ok`, `err`, `is`, `of`, `capture`), so a game can
|
|
62
|
+
--- build library-shaped results in its own wrappers. `result.err` only accepts
|
|
63
|
+
--- an `Error`, so a malformed result cannot be fabricated with it.
|
|
64
|
+
dataforge.result = result
|
|
65
|
+
|
|
48
66
|
dataforge.hooks = {
|
|
49
67
|
memory = memory_hook,
|
|
50
68
|
roblox = roblox_hook,
|
package/src/mutex.luau
CHANGED
|
@@ -24,7 +24,7 @@ end
|
|
|
24
24
|
|
|
25
25
|
function mutex.lock(self: Mutex)
|
|
26
26
|
if self._closed then
|
|
27
|
-
error(self._closed,
|
|
27
|
+
error(self._closed, 0)
|
|
28
28
|
end
|
|
29
29
|
if not self._locked then
|
|
30
30
|
self._locked = true
|
|
@@ -34,7 +34,7 @@ function mutex.lock(self: Mutex)
|
|
|
34
34
|
-- resumed with a reason only when the mutex was closed while waiting
|
|
35
35
|
local rejection = coroutine.yield()
|
|
36
36
|
if rejection ~= nil then
|
|
37
|
-
error(rejection,
|
|
37
|
+
error(rejection, 0)
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
|
|
@@ -59,9 +59,9 @@ function mutex.unlock(self: Mutex)
|
|
|
59
59
|
self.scheduler.spawn(next_waiter)
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
--- Rejects every current and future lock with `reason
|
|
63
|
-
--- close keeps the first reason.
|
|
64
|
-
function mutex.close(self: Mutex, reason:
|
|
62
|
+
--- Rejects every current and future lock with `reason` (an `Error` table,
|
|
63
|
+
--- thrown as is). Idempotent: a second close keeps the first reason.
|
|
64
|
+
function mutex.close(self: Mutex, reason: any)
|
|
65
65
|
if self._closed then
|
|
66
66
|
return
|
|
67
67
|
end
|
package/src/profile/common.luau
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
local errors = require "../errors"
|
|
1
2
|
local types = require "../types"
|
|
2
3
|
|
|
3
4
|
type Array<T> = { T }
|
|
@@ -8,25 +9,33 @@ export type MigrationsResult = types.MigrationsResult
|
|
|
8
9
|
|
|
9
10
|
local common = {}
|
|
10
11
|
|
|
11
|
-
--- Throws right away when the profile is closed
|
|
12
|
-
---
|
|
12
|
+
--- Throws a `profile_closed` error right away when the profile is closed. Not
|
|
13
|
+
--- strictly needed (the closed mutex rejects the call anyway) but it fails
|
|
14
|
+
--- before queueing on the mutex.
|
|
13
15
|
function common.assert_open(self: ProfileBase)
|
|
14
16
|
if not self.open then
|
|
15
|
-
error(
|
|
17
|
+
error(errors.closed(self), 0)
|
|
16
18
|
end
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
--- Applies the declared migrations not yet in `applied` (in order). Pure, so it
|
|
20
|
-
--- is safe inside an update transform. Fails
|
|
21
|
-
---
|
|
22
|
-
|
|
22
|
+
--- is safe inside an update transform. Fails with `outdated` in `err` when the
|
|
23
|
+
--- record lists more migrations than declared (a newer server wrote it), or
|
|
24
|
+
--- `migration_mismatch` when a name in the applied prefix differs. An error
|
|
25
|
+
--- thrown by a migration's `apply` propagates as is.
|
|
26
|
+
function common.apply_migrations(
|
|
27
|
+
key: string,
|
|
28
|
+
data: any,
|
|
29
|
+
applied: { string },
|
|
30
|
+
migrations: Array<types.Migration>
|
|
31
|
+
): MigrationsResult
|
|
23
32
|
local applied_count = #applied
|
|
24
33
|
|
|
25
34
|
if applied_count > #migrations then
|
|
26
35
|
return {
|
|
27
36
|
applied = false,
|
|
28
37
|
success = false,
|
|
29
|
-
err =
|
|
38
|
+
err = errors.outdated(key, applied, common.all_migration_names(migrations)),
|
|
30
39
|
new_migrations = applied,
|
|
31
40
|
}
|
|
32
41
|
end
|
|
@@ -38,7 +47,13 @@ function common.apply_migrations(data: any, applied: { string }, migrations: Arr
|
|
|
38
47
|
return {
|
|
39
48
|
applied = false,
|
|
40
49
|
success = false,
|
|
41
|
-
err =
|
|
50
|
+
err = errors.migration_mismatch(key, {
|
|
51
|
+
index = index,
|
|
52
|
+
expected = expected,
|
|
53
|
+
actual = actual,
|
|
54
|
+
applied = applied,
|
|
55
|
+
declared = common.all_migration_names(migrations),
|
|
56
|
+
}),
|
|
42
57
|
new_migrations = applied,
|
|
43
58
|
}
|
|
44
59
|
end
|
|
@@ -67,7 +82,39 @@ function common.all_migration_names(migrations: Array<types.Migration>): { strin
|
|
|
67
82
|
return names
|
|
68
83
|
end
|
|
69
84
|
|
|
70
|
-
|
|
85
|
+
--- Whether the record lists more migrations than this server declares. Pure.
|
|
86
|
+
function common.is_outdated(record: types.DataRecord?, config: types.ResolvedConfig): boolean
|
|
87
|
+
if not record or not record.migrations then
|
|
88
|
+
return false
|
|
89
|
+
end
|
|
90
|
+
return #record.migrations > #config.migrations
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
function common.run_predicate(config: types.ResolvedConfig, key: string, data: any): (boolean, any?)
|
|
94
|
+
local predicate = config.predicate
|
|
95
|
+
if predicate == nil then
|
|
96
|
+
return true, nil
|
|
97
|
+
end
|
|
98
|
+
local ok, passed = pcall(predicate, data, key)
|
|
99
|
+
if not ok then
|
|
100
|
+
return false, passed
|
|
101
|
+
end
|
|
102
|
+
return not not passed, nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
--- Rethrows a failed `apply_migrations` with the profile attached to an
|
|
106
|
+
--- `outdated` error (a `migration_mismatch` has no profile field).
|
|
107
|
+
function common.raise_migrations(profile: ProfileBase, migrated: MigrationsResult)
|
|
108
|
+
local err: any = migrated.err
|
|
109
|
+
if err.type == "outdated" then
|
|
110
|
+
err.profile = profile
|
|
111
|
+
end
|
|
112
|
+
error(err, 0)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
--- Whether `lock_id` (the id of one acquisition) is the lock currently on
|
|
116
|
+
--- the record, so this acquisition may write it.
|
|
117
|
+
function common.is_lock_owned(record: types.DataRecord?, lock_id: string)
|
|
71
118
|
if not record then
|
|
72
119
|
return false
|
|
73
120
|
end
|
|
@@ -77,10 +124,12 @@ function common.is_lock_owned(record: types.DataRecord?, job_id: string)
|
|
|
77
124
|
return false
|
|
78
125
|
end
|
|
79
126
|
|
|
80
|
-
return lock.id ==
|
|
127
|
+
return lock.id == lock_id
|
|
81
128
|
end
|
|
82
129
|
|
|
83
|
-
|
|
130
|
+
--- Whether a live lock of another acquisition holds the record, in which case
|
|
131
|
+
--- `lock_id` must wait it out or steal it once it expires.
|
|
132
|
+
function common.is_locked_for(record: types.DataRecord?, lock_id: string, now: number)
|
|
84
133
|
if not record then
|
|
85
134
|
return false
|
|
86
135
|
end
|
|
@@ -89,7 +138,7 @@ function common.is_locked_for(record: types.DataRecord?, job_id: string, now: nu
|
|
|
89
138
|
return false
|
|
90
139
|
end
|
|
91
140
|
|
|
92
|
-
return lock.id ~=
|
|
141
|
+
return lock.id ~= lock_id and lock.expires > now
|
|
93
142
|
end
|
|
94
143
|
|
|
95
144
|
function common.has_lock(record: types.DataRecord?)
|