@rbxts/dataforge 0.1.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/README.md +1 -0
- package/default.project.json +6 -0
- package/index.d.ts +423 -0
- package/package.json +32 -0
- package/src/hooks/memory.luau +186 -0
- package/src/hooks/roblox.luau +68 -0
- package/src/init.luau +58 -0
- package/src/mutex.luau +107 -0
- package/src/profile/common.luau +102 -0
- package/src/profile/lockless.luau +502 -0
- package/src/profile/peek.luau +79 -0
- package/src/profile/profile.luau +498 -0
- package/src/schedulers/roblox.luau +18 -0
- package/src/schedulers/virtual.luau +128 -0
- package/src/schedulers/zune.luau +18 -0
- package/src/store.luau +409 -0
- package/src/tx/handle.luau +427 -0
- package/src/tx/operation.luau +117 -0
- package/src/types.luau +375 -0
- package/src/utils.luau +104 -0
package/src/types.luau
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
type Array<T> = { T }
|
|
2
|
+
|
|
3
|
+
export type Counter = () -> number
|
|
4
|
+
|
|
5
|
+
--- Returns the new data, or nil / false to leave it untouched.
|
|
6
|
+
export type DataDispatcher<T> = (T) -> (T | false)?
|
|
7
|
+
|
|
8
|
+
--- A storage handle for one named store. Deliberately not shaped like a
|
|
9
|
+
--- Roblox DataStore. All methods throw on failure.
|
|
10
|
+
export type DatastoreHandle = {
|
|
11
|
+
name: string,
|
|
12
|
+
|
|
13
|
+
get_async: (key: string) -> any?,
|
|
14
|
+
set_async: (key: string, value: any, user_ids: { number }?) -> (),
|
|
15
|
+
update_async: (key: string, transform: (old: any?) -> any?, user_ids: { number }?) -> any?,
|
|
16
|
+
remove_async: ((key: string) -> ())?,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type Hook = {
|
|
20
|
+
job_id: () -> string,
|
|
21
|
+
get_datastore: (name: string) -> DatastoreHandle,
|
|
22
|
+
--- Non-fatal problems (a hook that threw, a write that could not be
|
|
23
|
+
--- released) are reported here instead of a global `warn`.
|
|
24
|
+
warn: (message: string) -> (),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type Scheduler = {
|
|
28
|
+
spawn: (fn: ((...any) -> ...any) | thread, ...any) -> thread,
|
|
29
|
+
defer: (fn: ((...any) -> ...any) | thread, ...any) -> thread,
|
|
30
|
+
wait: (seconds: number?) -> number,
|
|
31
|
+
now: () -> number,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type Migration = {
|
|
35
|
+
name: string,
|
|
36
|
+
apply: (data: unknown) -> unknown,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type Config<T = any> = {
|
|
40
|
+
name: string,
|
|
41
|
+
template: T,
|
|
42
|
+
|
|
43
|
+
_hook: Hook?,
|
|
44
|
+
_scheduler: Scheduler?,
|
|
45
|
+
|
|
46
|
+
migrations: Array<Migration>?,
|
|
47
|
+
|
|
48
|
+
--- seconds, default 30 (locked profiles)
|
|
49
|
+
autosave_interval: number?,
|
|
50
|
+
-- seconds, default autosave_interval (lockless profiles)
|
|
51
|
+
flush_interval: number?,
|
|
52
|
+
--- seconds, default 60
|
|
53
|
+
lock_ttl: number?,
|
|
54
|
+
--- seconds, default lock_ttl + 10
|
|
55
|
+
load_timeout: number?,
|
|
56
|
+
--- seconds, default 1
|
|
57
|
+
load_poll: number?,
|
|
58
|
+
--- default 5
|
|
59
|
+
retry_attempts: number?, -- default 5
|
|
60
|
+
--- seconds, default 1
|
|
61
|
+
retry_base: number?,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
--- Options shared by the retry helper. Both `ResolvedConfig` and
|
|
65
|
+
--- `ResolvedTransactionConfig` satisfy it.
|
|
66
|
+
export type RetryConfig = {
|
|
67
|
+
_scheduler: Scheduler,
|
|
68
|
+
retry_attempts: number,
|
|
69
|
+
retry_base: number,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type ResolvedConfig<T = any> = RetryConfig & {
|
|
73
|
+
name: string,
|
|
74
|
+
template: T,
|
|
75
|
+
|
|
76
|
+
_hook: Hook,
|
|
77
|
+
_scheduler: Scheduler,
|
|
78
|
+
|
|
79
|
+
migrations: Array<Migration>,
|
|
80
|
+
|
|
81
|
+
autosave_interval: number,
|
|
82
|
+
flush_interval: number,
|
|
83
|
+
lock_ttl: number,
|
|
84
|
+
load_timeout: number,
|
|
85
|
+
load_poll: number,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type TransactionConfig = {
|
|
89
|
+
_hook: Hook?,
|
|
90
|
+
_scheduler: Scheduler?,
|
|
91
|
+
_datastore: DatastoreHandle?,
|
|
92
|
+
|
|
93
|
+
--- seconds, default 60. Grace period others give this transaction's `pending`
|
|
94
|
+
--- before force-resolving it (stamped on the pending as `expires`).
|
|
95
|
+
tx_ttl: number?,
|
|
96
|
+
--- seconds, default 10. How long a transaction holds a lockless key between
|
|
97
|
+
--- taking its snapshot and writing `pending`.
|
|
98
|
+
lock_ttl: number?,
|
|
99
|
+
--- default 5
|
|
100
|
+
retry_attempts: number?,
|
|
101
|
+
--- seconds, default 1
|
|
102
|
+
retry_base: number?,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type ResolvedTransactionConfig = RetryConfig & {
|
|
106
|
+
_hook: Hook,
|
|
107
|
+
_scheduler: Scheduler,
|
|
108
|
+
--- Global marker datastore, resolved once via `tx_handle.get_tx_store`.
|
|
109
|
+
_datastore: DatastoreHandle,
|
|
110
|
+
--- Per-process transaction id counter (ids are `{job_id}-{n}`).
|
|
111
|
+
_counter: Counter,
|
|
112
|
+
|
|
113
|
+
tx_ttl: number,
|
|
114
|
+
lock_ttl: number,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export type Lock = {
|
|
118
|
+
id: string,
|
|
119
|
+
expires: number,
|
|
120
|
+
--- Short-lived lock a transaction takes on a lockless key (see `lock_ttl`
|
|
121
|
+
--- in `TransactionConfig`). Other lockless writers wait it out instead of failing.
|
|
122
|
+
tx: boolean?,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export type Pending<T = any> = {
|
|
126
|
+
tx_id: string,
|
|
127
|
+
store: string,
|
|
128
|
+
--- Until when a non-owner leaves this pending alone; after it, the marker
|
|
129
|
+
--- may be force-resolved (fallback "aborted").
|
|
130
|
+
expires: number,
|
|
131
|
+
data: T,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
--- What is stored under each data key.
|
|
135
|
+
export type DataRecord<T = any> = {
|
|
136
|
+
data: T?,
|
|
137
|
+
lock: Lock?,
|
|
138
|
+
pending: Pending<T>?,
|
|
139
|
+
migrations: { string },
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
--- FIFO mutex for coroutines. See `mutex.luau`.
|
|
143
|
+
export type Mutex = {
|
|
144
|
+
_locked: boolean,
|
|
145
|
+
_closed: string?,
|
|
146
|
+
_waiters: { thread },
|
|
147
|
+
_settlers: { thread },
|
|
148
|
+
|
|
149
|
+
scheduler: Scheduler,
|
|
150
|
+
|
|
151
|
+
wait_settled: (self: Mutex) -> (),
|
|
152
|
+
lock: (self: Mutex) -> (),
|
|
153
|
+
unlock: (self: Mutex) -> (),
|
|
154
|
+
close: (self: Mutex, reason: string) -> (),
|
|
155
|
+
is_locked: (self: Mutex) -> boolean,
|
|
156
|
+
is_closed: (self: Mutex) -> boolean,
|
|
157
|
+
with: (self: Mutex, fn: () -> ...any) -> any,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export type TxOutcome = "committed" | "aborted"
|
|
161
|
+
|
|
162
|
+
--- One participant's share of a multi-key transaction. Built by `tx_handle.run`
|
|
163
|
+
--- per profile kind; the operation drives it through prepare / commit / abort.
|
|
164
|
+
export type TxPatch = {
|
|
165
|
+
--- A `ProfileBase`. Typed `any` on purpose: `ProfileBase<T>` refers back to
|
|
166
|
+
--- `TxOperation`, and Luau rejects a generic type used with different
|
|
167
|
+
--- parameters inside one recursive group.
|
|
168
|
+
profile: any,
|
|
169
|
+
new_data: any,
|
|
170
|
+
--- Set once `prepare` succeeded (the record carries `pending`).
|
|
171
|
+
prepared: boolean,
|
|
172
|
+
|
|
173
|
+
--- Phase 1: persist `pending` next to the current data. Returns false when
|
|
174
|
+
--- the profile no longer owns its lock. May throw.
|
|
175
|
+
prepare: (pending: Pending) -> boolean,
|
|
176
|
+
--- Applies the outcome in memory. Called exactly once by the operation.
|
|
177
|
+
commit: (operation: TxOperation) -> (),
|
|
178
|
+
abort: (operation: TxOperation) -> (),
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export type TxContext = {
|
|
182
|
+
get: <T>(self: TxContext, profile: ProfileBase<T>) -> T,
|
|
183
|
+
set: <T>(self: TxContext, profile: ProfileBase<T>, new: T) -> (),
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
--- in-flight multi-key transaction.
|
|
187
|
+
export type TxOperation = {
|
|
188
|
+
_resolving: boolean,
|
|
189
|
+
_applied: boolean,
|
|
190
|
+
_waiters: { thread },
|
|
191
|
+
_unclean: number,
|
|
192
|
+
|
|
193
|
+
id: string,
|
|
194
|
+
config: ResolvedTransactionConfig,
|
|
195
|
+
desired: TxOutcome,
|
|
196
|
+
outcome: TxOutcome?,
|
|
197
|
+
|
|
198
|
+
patches: { TxPatch },
|
|
199
|
+
|
|
200
|
+
--- Resolves the marker (blocking) and applies the outcome to every patch once.
|
|
201
|
+
settle: (self: TxOperation) -> TxOutcome,
|
|
202
|
+
--- A participant wrote a clean record; once all did, the marker is removed.
|
|
203
|
+
cleaned: (self: TxOperation) -> (),
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
--[[
|
|
207
|
+
Profiles
|
|
208
|
+
]]
|
|
209
|
+
|
|
210
|
+
export type HooksList<T... = ()> = {
|
|
211
|
+
type: string,
|
|
212
|
+
hooks: Array<(T...) -> ()>,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export type ProfileBase<T = any> = {
|
|
216
|
+
kind: "locked" | "lockless",
|
|
217
|
+
key: string,
|
|
218
|
+
config: ResolvedConfig<T>,
|
|
219
|
+
user_ids: Array<number>,
|
|
220
|
+
_datastore: DatastoreHandle,
|
|
221
|
+
open: boolean,
|
|
222
|
+
migrations: Array<string>,
|
|
223
|
+
|
|
224
|
+
--- frozen; nil until loaded (locked) / fetched (lockless). Read through `get_data()`.
|
|
225
|
+
_data: T?,
|
|
226
|
+
_mutex: Mutex,
|
|
227
|
+
--- Operations waiting for this profile to write a record without `pending`.
|
|
228
|
+
_cleanup_txs: Array<TxOperation>,
|
|
229
|
+
|
|
230
|
+
--- Current data. Throws if the profile is closed or (lockless) not fetched.
|
|
231
|
+
get_data: (self: ProfileBase<T>) -> T,
|
|
232
|
+
save: (self: ProfileBase<T>) -> (),
|
|
233
|
+
unload: (self: ProfileBase<T>) -> (),
|
|
234
|
+
update: (self: ProfileBase<T>, (T) -> T | false) -> boolean,
|
|
235
|
+
|
|
236
|
+
on_change: (self: ProfileBase<T>, callback: (new: T, old: T?) -> ()) -> (),
|
|
237
|
+
on_save: (self: ProfileBase<T>, callback: () -> ()) -> (),
|
|
238
|
+
--- Fires at the start of `unload`, while the profile is still open: the
|
|
239
|
+
--- last chance to update before the final write.
|
|
240
|
+
on_closing: (self: ProfileBase<T>, callback: () -> ()) -> (),
|
|
241
|
+
--- Fires once the profile is closed and its final write (if any) is done.
|
|
242
|
+
--- Also fires after `lock_lost`.
|
|
243
|
+
on_closed: (self: ProfileBase<T>, callback: () -> ()) -> (),
|
|
244
|
+
on_lock_lost: (self: ProfileBase<T>, callback: () -> ()) -> (),
|
|
245
|
+
|
|
246
|
+
--- Yields until no operation holds or waits on the profile mutex. Once this
|
|
247
|
+
--- returns, the next update call is guaranteed not to yield (nothing is
|
|
248
|
+
--- queued ahead of it).
|
|
249
|
+
wait_settled: (self: ProfileBase<T>) -> (),
|
|
250
|
+
|
|
251
|
+
-- internal, used by transactions
|
|
252
|
+
_set_data: (self: ProfileBase<T>, data: T) -> (),
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
--- Session-locked profile.
|
|
256
|
+
export type Profile<T = any> = ProfileBase<T> & {
|
|
257
|
+
kind: "locked",
|
|
258
|
+
last_write: number,
|
|
259
|
+
--- Whether the profile currently holds its session lock. True after `load`;
|
|
260
|
+
--- `release` clears it, `readquire` takes it again. While released,
|
|
261
|
+
--- `update` / `save` throw and the profile cannot join a transaction.
|
|
262
|
+
is_locked: boolean,
|
|
263
|
+
|
|
264
|
+
_dirty: boolean,
|
|
265
|
+
_hooks: {
|
|
266
|
+
change: HooksList<T, T?>,
|
|
267
|
+
lock_lost: HooksList<()>,
|
|
268
|
+
save: HooksList<()>,
|
|
269
|
+
closing: HooksList<()>,
|
|
270
|
+
closed: HooksList<()>,
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
wait_closed: (self: Profile<T>) -> (),
|
|
274
|
+
|
|
275
|
+
--- Writes the data and gives the session lock back, keeping the profile
|
|
276
|
+
--- open so another server may take the key. No-op when already released.
|
|
277
|
+
release: (self: Profile<T>) -> (),
|
|
278
|
+
--- Takes the session lock again (waits like `load` does) and adopts the
|
|
279
|
+
--- stored record as the current data. No-op when already locked.
|
|
280
|
+
readquire: (self: Profile<T>) -> (),
|
|
281
|
+
|
|
282
|
+
-- internal, used by transactions. Returns false when the lock is gone.
|
|
283
|
+
_write_record: (self: Profile<T>, options: ProfileWriteOptions) -> boolean,
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export type ProfileWriteOptions = {
|
|
287
|
+
release: boolean,
|
|
288
|
+
pending: Pending?,
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
--- Profile without a session lock; updates are queued and folded into storage on save.
|
|
292
|
+
export type LocklessProfile<T = any> = ProfileBase<T> & {
|
|
293
|
+
kind: "lockless",
|
|
294
|
+
fetched: boolean,
|
|
295
|
+
|
|
296
|
+
queued_updates: Array<DataDispatcher<T>>,
|
|
297
|
+
dirty_pending: boolean,
|
|
298
|
+
--- The transaction lock this profile believes storage holds (taken by
|
|
299
|
+
--- `_write_queued { tx_lock = lock }`, cleared by a write passing it as
|
|
300
|
+
--- `old_tx_lock`). nil when no lock is held.
|
|
301
|
+
_tx_lock: Lock?,
|
|
302
|
+
--- Outcomes of transactions whose `pending` this profile has seen on its record.
|
|
303
|
+
_known_outcomes: { [string]: TxOutcome },
|
|
304
|
+
|
|
305
|
+
_hooks: {
|
|
306
|
+
change: Array<(new: T, old: T?) -> ()>,
|
|
307
|
+
save: Array<() -> ()>,
|
|
308
|
+
closing: Array<() -> ()>,
|
|
309
|
+
closed: Array<() -> ()>,
|
|
310
|
+
--- A session lock showed up on the record (a locked profile owns the key
|
|
311
|
+
--- now); the profile closes.
|
|
312
|
+
lock_lost: Array<() -> ()>,
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
fetch: (self: LocklessProfile<T>) -> T,
|
|
316
|
+
|
|
317
|
+
-- internal, used by transactions
|
|
318
|
+
_write_queued: (self: LocklessProfile<T>, options: LocklessWriteOptions) -> (),
|
|
319
|
+
_queue_update: (self: LocklessProfile<T>, dispatcher: DataDispatcher<T>) -> boolean,
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
--- Read-only snapshot of a key taken with `get_async`. Not a `ProfileBase`:
|
|
323
|
+
--- it cannot write, hold a lock or join a transaction. Returned by `store:peek`.
|
|
324
|
+
export type PeekProfile<T = any> = {
|
|
325
|
+
kind: "peek",
|
|
326
|
+
key: string,
|
|
327
|
+
_config: ResolvedConfig<T>,
|
|
328
|
+
_datastore: DatastoreHandle,
|
|
329
|
+
|
|
330
|
+
--- Lock currently on the record (session or transaction), nil when free.
|
|
331
|
+
lock: Lock?,
|
|
332
|
+
--- Transaction marker on the record, if any. Left as stored, never resolved.
|
|
333
|
+
pending: Pending<T>?,
|
|
334
|
+
--- Migration names after the in-memory migrations of the last read.
|
|
335
|
+
migrations: Array<string>,
|
|
336
|
+
|
|
337
|
+
--- Frozen. The template when the key does not exist.
|
|
338
|
+
_data: T,
|
|
339
|
+
|
|
340
|
+
get_data: (self: PeekProfile<T>) -> T,
|
|
341
|
+
--- Calls `GetAsync` again and replaces `lock`, `pending`, `migrations` and
|
|
342
|
+
--- the data. Yields.
|
|
343
|
+
refresh: (self: PeekProfile<T>) -> T,
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export type LocklessWriteOptions = {
|
|
347
|
+
--- Leave this transaction lock on the record with this write (built by the
|
|
348
|
+
--- transaction from its own config).
|
|
349
|
+
tx_lock: Lock?,
|
|
350
|
+
--- The transaction lock expected on the record. The write only goes through
|
|
351
|
+
--- (replacing that lock) when the stored lock has the same id; if the lock
|
|
352
|
+
--- is gone or is a different one, nothing is written and the call fails
|
|
353
|
+
--- with "lock_lost". Without it, any lock on the record blocks the write.
|
|
354
|
+
old_tx_lock: Lock?,
|
|
355
|
+
--- Persist `pending` with this write.
|
|
356
|
+
pending: Pending?,
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
--- What a store hands to `profile.load` / `lockless_profile.create`.
|
|
360
|
+
export type LoadOptions = {
|
|
361
|
+
key: string,
|
|
362
|
+
_datastore: DatastoreHandle,
|
|
363
|
+
config: ResolvedConfig,
|
|
364
|
+
user_ids: Array<number>,
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export type MigrationsResult = {
|
|
368
|
+
applied: boolean,
|
|
369
|
+
success: boolean,
|
|
370
|
+
err: any,
|
|
371
|
+
result: any,
|
|
372
|
+
new_migrations: { string },
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return nil
|
package/src/utils.luau
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
local types = require "./types"
|
|
2
|
+
|
|
3
|
+
export type Counter = types.Counter
|
|
4
|
+
|
|
5
|
+
local utils = {}
|
|
6
|
+
|
|
7
|
+
--- Recursively freezes a table (and nested tables). Non-table values are returned as-is.
|
|
8
|
+
function utils.deep_freeze<T>(value: T): T
|
|
9
|
+
if type(value) ~= "table" then
|
|
10
|
+
return value
|
|
11
|
+
end
|
|
12
|
+
local tbl = value :: any
|
|
13
|
+
if table.isfrozen(tbl) then
|
|
14
|
+
return value
|
|
15
|
+
end
|
|
16
|
+
for _, child in tbl do
|
|
17
|
+
if type(child) == "table" then
|
|
18
|
+
utils.deep_freeze(child)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
table.freeze(tbl :: {})
|
|
22
|
+
return value
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
--- Recursively copies a table. The copy is not frozen.
|
|
26
|
+
function utils.deep_copy<T>(value: T): T
|
|
27
|
+
if type(value) ~= "table" then
|
|
28
|
+
return value
|
|
29
|
+
end
|
|
30
|
+
local copy = {}
|
|
31
|
+
for key, child in value :: any do
|
|
32
|
+
copy[key] = utils.deep_copy(child)
|
|
33
|
+
end
|
|
34
|
+
return copy :: any
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
--- Returns true when both tables have exactly the same set of keys.
|
|
38
|
+
function utils.same_key_set(a: { [any]: any }, b: { [any]: any }): boolean
|
|
39
|
+
for key in a do
|
|
40
|
+
if b[key] == nil then
|
|
41
|
+
return false
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
for key in b do
|
|
45
|
+
if a[key] == nil then
|
|
46
|
+
return false
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
return true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
--- pcall that always yields exactly (ok, err_or_first_result), even when `fn` returns nothing.
|
|
53
|
+
function utils.try(fn: (...any) -> ...any, ...: any): (boolean, any)
|
|
54
|
+
local results = { pcall(fn, ...) }
|
|
55
|
+
return results[1], results[2]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
--- Shallow copy of an array sorted with the provided comparator.
|
|
59
|
+
function utils.sorted<T>(list: { T }, less: (T, T) -> boolean): { T }
|
|
60
|
+
local copy = table.clone(list)
|
|
61
|
+
table.sort(copy, less)
|
|
62
|
+
return copy
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
local HEX = "0123456789abcdef"
|
|
66
|
+
|
|
67
|
+
--- Random 16-char hex id. Collisions are astronomically unlikely, Alternative to httpservice.
|
|
68
|
+
function utils.uuid(): string
|
|
69
|
+
local chars = table.create(16)
|
|
70
|
+
for index = 1, 16 do
|
|
71
|
+
local nibble = math.random(1, 16)
|
|
72
|
+
chars[index] = string.sub(HEX, nibble, nibble)
|
|
73
|
+
end
|
|
74
|
+
return table.concat(chars)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
function utils.retry<T>(config: types.RetryConfig, callback: () -> T): T
|
|
78
|
+
local last_error: any = nil
|
|
79
|
+
local attempts = config.retry_attempts
|
|
80
|
+
local base = config.retry_base
|
|
81
|
+
|
|
82
|
+
for attempt = 1, attempts do
|
|
83
|
+
local ok, result = pcall(callback)
|
|
84
|
+
if ok then
|
|
85
|
+
return result
|
|
86
|
+
end
|
|
87
|
+
last_error = result
|
|
88
|
+
if attempt < attempts then
|
|
89
|
+
config._scheduler.wait(base * 2 ^ (attempt - 1))
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
error(last_error, 0)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
function utils.counter(): Counter
|
|
96
|
+
local count = 0
|
|
97
|
+
return function()
|
|
98
|
+
local current = count
|
|
99
|
+
count += 1
|
|
100
|
+
return current
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
return utils
|