@rbxts/dataforge 0.1.0 → 0.2.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/index.d.ts +218 -26
- package/package.json +1 -1
- package/src/errors.luau +195 -0
- package/src/hooks/memory.luau +1 -12
- package/src/hooks/roblox.luau +10 -1
- package/src/init.luau +6 -2
- package/src/mutex.luau +5 -5
- package/src/profile/common.luau +41 -8
- package/src/profile/lockless.luau +102 -50
- package/src/profile/peek.luau +22 -16
- package/src/profile/profile.luau +156 -95
- package/src/result.luau +61 -0
- package/src/store.luau +89 -41
- package/src/tx/handle.luau +82 -59
- package/src/tx/operation.luau +6 -3
- package/src/types.luau +187 -16
- package/src/utils.luau +37 -3
package/src/types.luau
CHANGED
|
@@ -61,7 +61,7 @@ export type Config<T = any> = {
|
|
|
61
61
|
retry_base: number?,
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
--- Options shared by the retry
|
|
64
|
+
--- Options shared by the storage retry helpers. Both `ResolvedConfig` and
|
|
65
65
|
--- `ResolvedTransactionConfig` satisfy it.
|
|
66
66
|
export type RetryConfig = {
|
|
67
67
|
_scheduler: Scheduler,
|
|
@@ -139,10 +139,175 @@ export type DataRecord<T = any> = {
|
|
|
139
139
|
migrations: { string },
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
--[[
|
|
143
|
+
Errors and results
|
|
144
|
+
]]
|
|
145
|
+
|
|
146
|
+
--- Every runtime failure is one of these tables (see `errors.luau`): `type`
|
|
147
|
+
--- tells them apart, `message` is human readable (`tostring(err)` returns
|
|
148
|
+
--- it), the rest is typed extra data. `profile` / `store` fields are typed
|
|
149
|
+
--- `any` on purpose: naming `ProfileBase` or `Store` here would put them in a
|
|
150
|
+
--- recursive generic group with `Result`, which Luau rejects.
|
|
151
|
+
export type RobloxError = {
|
|
152
|
+
type: "roblox",
|
|
153
|
+
message: string,
|
|
154
|
+
op: "get_async" | "update_async",
|
|
155
|
+
store: string,
|
|
156
|
+
key: string,
|
|
157
|
+
--- the last raw error the storage hook threw
|
|
158
|
+
cause: any,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export type TimeoutError = {
|
|
162
|
+
type: "timeout",
|
|
163
|
+
message: string,
|
|
164
|
+
key: string,
|
|
165
|
+
--- the last lock seen on the record while waiting
|
|
166
|
+
lock: Lock?,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export type LockLostError = {
|
|
170
|
+
type: "lock_lost",
|
|
171
|
+
message: string,
|
|
172
|
+
profile: any,
|
|
173
|
+
--- the lock now on the record, nil when it has none
|
|
174
|
+
lock: Lock?,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export type ReleasedError = {
|
|
178
|
+
type: "released",
|
|
179
|
+
message: string,
|
|
180
|
+
profile: any,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export type LocklessLockedError = {
|
|
184
|
+
type: "lockless_locked",
|
|
185
|
+
message: string,
|
|
186
|
+
profile: any,
|
|
187
|
+
--- the live session lock another server holds on the record
|
|
188
|
+
lock: Lock,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export type TxLockLostError = {
|
|
192
|
+
type: "tx_lock_lost",
|
|
193
|
+
message: string,
|
|
194
|
+
profile: any,
|
|
195
|
+
--- the transaction lock this profile held
|
|
196
|
+
expected: Lock,
|
|
197
|
+
--- the lock now on the record, nil when it has none
|
|
198
|
+
lock: Lock?,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
--- The record lists more migrations than this server declares: a newer server
|
|
202
|
+
--- wrote it, so this one must not touch it (nothing is written, no lock taken).
|
|
203
|
+
export type OutdatedError = {
|
|
204
|
+
type: "outdated",
|
|
205
|
+
message: string,
|
|
206
|
+
key: string,
|
|
207
|
+
--- the profile the call ran on; nil for `store:load` and `store:peek`
|
|
208
|
+
profile: any?,
|
|
209
|
+
record_migrations: Array<string>,
|
|
210
|
+
declared_migrations: Array<string>,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export type NotFetchedError = {
|
|
214
|
+
type: "not_fetched",
|
|
215
|
+
message: string,
|
|
216
|
+
profile: any,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export type ProfileClosedError = {
|
|
220
|
+
type: "profile_closed",
|
|
221
|
+
message: string,
|
|
222
|
+
profile: any,
|
|
223
|
+
reason: "unloaded" | "lock_lost",
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
--- A name in the record's applied migrations differs from the declared one
|
|
227
|
+
--- at `index`. A record with more migrations than declared is `outdated`.
|
|
228
|
+
export type MigrationMismatchError = {
|
|
229
|
+
type: "migration_mismatch",
|
|
230
|
+
message: string,
|
|
231
|
+
key: string,
|
|
232
|
+
--- the position that differs, and the declared / record's name there
|
|
233
|
+
index: number,
|
|
234
|
+
expected: string,
|
|
235
|
+
actual: string,
|
|
236
|
+
--- names the record has applied / this server declares
|
|
237
|
+
applied: Array<string>,
|
|
238
|
+
declared: Array<string>,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export type TxAbortedError = {
|
|
242
|
+
type: "tx_aborted",
|
|
243
|
+
message: string,
|
|
244
|
+
id: string,
|
|
245
|
+
--- the failure that made phase 1 give up; nil when the marker was already
|
|
246
|
+
--- resolved as aborted by another server
|
|
247
|
+
cause: Error?,
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export type TxMarkerInvalidError = {
|
|
251
|
+
type: "tx_marker_invalid",
|
|
252
|
+
message: string,
|
|
253
|
+
id: string,
|
|
254
|
+
value: any,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export type StoreClosedError = {
|
|
258
|
+
type: "store_closed",
|
|
259
|
+
message: string,
|
|
260
|
+
store: any,
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export type AlreadyLoadedError = {
|
|
264
|
+
type: "already_loaded",
|
|
265
|
+
message: string,
|
|
266
|
+
store: any,
|
|
267
|
+
key: string,
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export type Error =
|
|
271
|
+
| RobloxError
|
|
272
|
+
| TimeoutError
|
|
273
|
+
| LockLostError
|
|
274
|
+
| ReleasedError
|
|
275
|
+
| LocklessLockedError
|
|
276
|
+
| TxLockLostError
|
|
277
|
+
| OutdatedError
|
|
278
|
+
| NotFetchedError
|
|
279
|
+
| ProfileClosedError
|
|
280
|
+
| MigrationMismatchError
|
|
281
|
+
| TxAbortedError
|
|
282
|
+
| TxMarkerInvalidError
|
|
283
|
+
| StoreClosedError
|
|
284
|
+
| AlreadyLoadedError
|
|
285
|
+
|
|
286
|
+
export type Ok<T> = {
|
|
287
|
+
success: true,
|
|
288
|
+
value: T,
|
|
289
|
+
--- Returns `value`.
|
|
290
|
+
unwrap: (self: any) -> T,
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export type Err = {
|
|
294
|
+
success: false,
|
|
295
|
+
error: Error,
|
|
296
|
+
--- Throws `error` (the table itself; `tostring` gives its message).
|
|
297
|
+
--- Typed `any`: Luau cannot call a union of two function types, and this
|
|
298
|
+
--- keeps `result:unwrap()` callable on `Result<T>` while `success` still
|
|
299
|
+
--- narrows the union.
|
|
300
|
+
unwrap: any,
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
--- What every fallible library call returns. See `result.luau`.
|
|
304
|
+
export type Result<T> = Ok<T> | Err
|
|
305
|
+
|
|
142
306
|
--- FIFO mutex for coroutines. See `mutex.luau`.
|
|
143
307
|
export type Mutex = {
|
|
144
308
|
_locked: boolean,
|
|
145
|
-
|
|
309
|
+
--- The `Error` (a `profile_closed`) every lock is rejected with once closed.
|
|
310
|
+
_closed: any?,
|
|
146
311
|
_waiters: { thread },
|
|
147
312
|
_settlers: { thread },
|
|
148
313
|
|
|
@@ -151,7 +316,7 @@ export type Mutex = {
|
|
|
151
316
|
wait_settled: (self: Mutex) -> (),
|
|
152
317
|
lock: (self: Mutex) -> (),
|
|
153
318
|
unlock: (self: Mutex) -> (),
|
|
154
|
-
close: (self: Mutex, reason:
|
|
319
|
+
close: (self: Mutex, reason: any) -> (),
|
|
155
320
|
is_locked: (self: Mutex) -> boolean,
|
|
156
321
|
is_closed: (self: Mutex) -> boolean,
|
|
157
322
|
with: (self: Mutex, fn: () -> ...any) -> any,
|
|
@@ -170,9 +335,9 @@ export type TxPatch = {
|
|
|
170
335
|
--- Set once `prepare` succeeded (the record carries `pending`).
|
|
171
336
|
prepared: boolean,
|
|
172
337
|
|
|
173
|
-
--- Phase 1: persist `pending` next to the current data.
|
|
174
|
-
---
|
|
175
|
-
prepare: (pending: Pending) ->
|
|
338
|
+
--- Phase 1: persist `pending` next to the current data. Throws an `Error`
|
|
339
|
+
--- (`lock_lost`, `tx_lock_lost`, `lockless_locked`, `roblox`, ...) on failure.
|
|
340
|
+
prepare: (pending: Pending) -> (),
|
|
176
341
|
--- Applies the outcome in memory. Called exactly once by the operation.
|
|
177
342
|
commit: (operation: TxOperation) -> (),
|
|
178
343
|
abort: (operation: TxOperation) -> (),
|
|
@@ -227,11 +392,14 @@ export type ProfileBase<T = any> = {
|
|
|
227
392
|
--- Operations waiting for this profile to write a record without `pending`.
|
|
228
393
|
_cleanup_txs: Array<TxOperation>,
|
|
229
394
|
|
|
230
|
-
--- Current data. Throws
|
|
395
|
+
--- Current data. Throws an `Error` (`profile_closed`, or `not_fetched` for
|
|
396
|
+
--- a lockless profile) instead of returning a Result: it is a plain accessor.
|
|
231
397
|
get_data: (self: ProfileBase<T>) -> T,
|
|
232
|
-
save: (self: ProfileBase<T>) ->
|
|
398
|
+
save: (self: ProfileBase<T>) -> Result<nil>,
|
|
233
399
|
unload: (self: ProfileBase<T>) -> (),
|
|
234
|
-
|
|
400
|
+
--- Ok(true) when the data changed, Ok(false) when the dispatcher returned
|
|
401
|
+
--- nil / false. An error thrown by the dispatcher propagates as is.
|
|
402
|
+
update: (self: ProfileBase<T>, (T) -> T | false) -> Result<boolean>,
|
|
235
403
|
|
|
236
404
|
on_change: (self: ProfileBase<T>, callback: (new: T, old: T?) -> ()) -> (),
|
|
237
405
|
on_save: (self: ProfileBase<T>, callback: () -> ()) -> (),
|
|
@@ -274,13 +442,14 @@ export type Profile<T = any> = ProfileBase<T> & {
|
|
|
274
442
|
|
|
275
443
|
--- Writes the data and gives the session lock back, keeping the profile
|
|
276
444
|
--- open so another server may take the key. No-op when already released.
|
|
277
|
-
release: (self: Profile<T>) ->
|
|
445
|
+
release: (self: Profile<T>) -> Result<nil>,
|
|
278
446
|
--- Takes the session lock again (waits like `load` does) and adopts the
|
|
279
447
|
--- stored record as the current data. No-op when already locked.
|
|
280
|
-
readquire: (self: Profile<T>) ->
|
|
448
|
+
readquire: (self: Profile<T>) -> Result<nil>,
|
|
281
449
|
|
|
282
|
-
-- internal, used by transactions. Returns false
|
|
283
|
-
|
|
450
|
+
-- internal, used by transactions. Returns false (and the lock now on the
|
|
451
|
+
-- record) when the lock is gone.
|
|
452
|
+
_write_record: (self: Profile<T>, options: ProfileWriteOptions) -> (boolean, Lock?),
|
|
284
453
|
}
|
|
285
454
|
|
|
286
455
|
export type ProfileWriteOptions = {
|
|
@@ -312,7 +481,7 @@ export type LocklessProfile<T = any> = ProfileBase<T> & {
|
|
|
312
481
|
lock_lost: Array<() -> ()>,
|
|
313
482
|
},
|
|
314
483
|
|
|
315
|
-
fetch: (self: LocklessProfile<T>) -> T
|
|
484
|
+
fetch: (self: LocklessProfile<T>) -> Result<T>,
|
|
316
485
|
|
|
317
486
|
-- internal, used by transactions
|
|
318
487
|
_write_queued: (self: LocklessProfile<T>, options: LocklessWriteOptions) -> (),
|
|
@@ -340,7 +509,7 @@ export type PeekProfile<T = any> = {
|
|
|
340
509
|
get_data: (self: PeekProfile<T>) -> T,
|
|
341
510
|
--- Calls `GetAsync` again and replaces `lock`, `pending`, `migrations` and
|
|
342
511
|
--- the data. Yields.
|
|
343
|
-
refresh: (self: PeekProfile<T>) -> T
|
|
512
|
+
refresh: (self: PeekProfile<T>) -> Result<T>,
|
|
344
513
|
}
|
|
345
514
|
|
|
346
515
|
export type LocklessWriteOptions = {
|
|
@@ -367,7 +536,9 @@ export type LoadOptions = {
|
|
|
367
536
|
export type MigrationsResult = {
|
|
368
537
|
applied: boolean,
|
|
369
538
|
success: boolean,
|
|
370
|
-
|
|
539
|
+
--- `outdated` when the record lists more migrations than declared,
|
|
540
|
+
--- `migration_mismatch` when a name differs
|
|
541
|
+
err: (MigrationMismatchError | OutdatedError)?,
|
|
371
542
|
result: any,
|
|
372
543
|
new_migrations: { string },
|
|
373
544
|
}
|
package/src/utils.luau
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
local errors = require "./errors"
|
|
1
2
|
local types = require "./types"
|
|
2
3
|
|
|
3
4
|
export type Counter = types.Counter
|
|
@@ -74,7 +75,9 @@ function utils.uuid(): string
|
|
|
74
75
|
return table.concat(chars)
|
|
75
76
|
end
|
|
76
77
|
|
|
77
|
-
|
|
78
|
+
--- Calls `callback` up to `retry_attempts` times with exponential backoff.
|
|
79
|
+
--- Returns the result, or false and the last error once every attempt failed.
|
|
80
|
+
local function retry<T>(config: types.RetryConfig, callback: () -> T): (boolean, T | any)
|
|
78
81
|
local last_error: any = nil
|
|
79
82
|
local attempts = config.retry_attempts
|
|
80
83
|
local base = config.retry_base
|
|
@@ -82,14 +85,45 @@ function utils.retry<T>(config: types.RetryConfig, callback: () -> T): T
|
|
|
82
85
|
for attempt = 1, attempts do
|
|
83
86
|
local ok, result = pcall(callback)
|
|
84
87
|
if ok then
|
|
85
|
-
return result
|
|
88
|
+
return true, result
|
|
86
89
|
end
|
|
87
90
|
last_error = result
|
|
88
91
|
if attempt < attempts then
|
|
89
92
|
config._scheduler.wait(base * 2 ^ (attempt - 1))
|
|
90
93
|
end
|
|
91
94
|
end
|
|
92
|
-
|
|
95
|
+
return false, last_error
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
--- `datastore.get_async(key)` under the retry policy. Throws a `roblox`
|
|
99
|
+
--- error once every attempt failed.
|
|
100
|
+
function utils.get_async(config: types.RetryConfig, datastore: types.DatastoreHandle, key: string): any?
|
|
101
|
+
local ok, result = retry(config, function()
|
|
102
|
+
return datastore.get_async(key)
|
|
103
|
+
end)
|
|
104
|
+
if not ok then
|
|
105
|
+
error(errors.roblox("get_async", datastore.name, key, result), 0)
|
|
106
|
+
end
|
|
107
|
+
return result
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
--- `datastore.update_async(key, transform, user_ids)` under the retry policy.
|
|
111
|
+
--- Throws a `roblox` error once every attempt failed. `transform` must not
|
|
112
|
+
--- throw: whatever it throws counts as a storage failure and is retried.
|
|
113
|
+
function utils.update_async(
|
|
114
|
+
config: types.RetryConfig,
|
|
115
|
+
datastore: types.DatastoreHandle,
|
|
116
|
+
key: string,
|
|
117
|
+
transform: (old: any?) -> any?,
|
|
118
|
+
user_ids: { number }?
|
|
119
|
+
): any?
|
|
120
|
+
local ok, result = retry(config, function()
|
|
121
|
+
return datastore.update_async(key, transform, user_ids)
|
|
122
|
+
end)
|
|
123
|
+
if not ok then
|
|
124
|
+
error(errors.roblox("update_async", datastore.name, key, result), 0)
|
|
125
|
+
end
|
|
126
|
+
return result
|
|
93
127
|
end
|
|
94
128
|
|
|
95
129
|
function utils.counter(): Counter
|