@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/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,6 +82,24 @@ function common.all_migration_names(migrations: Array<types.Migration>): { strin
|
|
|
67
82
|
return names
|
|
68
83
|
end
|
|
69
84
|
|
|
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
|
+
--- Rethrows a failed `apply_migrations` with the profile attached to an
|
|
94
|
+
--- `outdated` error (a `migration_mismatch` has no profile field).
|
|
95
|
+
function common.raise_migrations(profile: ProfileBase, migrated: MigrationsResult)
|
|
96
|
+
local err: any = migrated.err
|
|
97
|
+
if err.type == "outdated" then
|
|
98
|
+
err.profile = profile
|
|
99
|
+
end
|
|
100
|
+
error(err, 0)
|
|
101
|
+
end
|
|
102
|
+
|
|
70
103
|
function common.is_lock_owned(record: types.DataRecord?, job_id: string)
|
|
71
104
|
if not record then
|
|
72
105
|
return false
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
local common = require "./common"
|
|
2
|
+
local errors = require "../errors"
|
|
2
3
|
local mutex = require "../mutex"
|
|
4
|
+
local result = require "../result"
|
|
3
5
|
local tx_handle = require "../tx/handle"
|
|
4
6
|
local types = require "../types"
|
|
5
7
|
local utils = require "../utils"
|
|
@@ -15,7 +17,7 @@ lockless_profile.__index = lockless_profile
|
|
|
15
17
|
function lockless_profile.get_data(self: LocklessProfile)
|
|
16
18
|
common.assert_open(self)
|
|
17
19
|
if not self.fetched then
|
|
18
|
-
error(
|
|
20
|
+
error(errors.not_fetched(self), 0)
|
|
19
21
|
end
|
|
20
22
|
return self._data
|
|
21
23
|
end
|
|
@@ -67,11 +69,11 @@ local function resolve_record(self: LocklessProfile, record: DataRecord?, data:
|
|
|
67
69
|
return utils.deep_copy(config.template), common.all_migration_names(config.migrations)
|
|
68
70
|
end
|
|
69
71
|
|
|
70
|
-
local
|
|
71
|
-
if not
|
|
72
|
-
|
|
72
|
+
local migrated = common.apply_migrations(self.key, data, applied, config.migrations)
|
|
73
|
+
if not migrated.success then
|
|
74
|
+
common.raise_migrations(self, migrated)
|
|
73
75
|
end
|
|
74
|
-
return
|
|
76
|
+
return migrated.result, migrated.new_migrations
|
|
75
77
|
end
|
|
76
78
|
|
|
77
79
|
local function replay(queue: Array<types.DataDispatcher<any>>, data: any): any
|
|
@@ -108,13 +110,15 @@ local function process_lock_lost(self: LocklessProfile)
|
|
|
108
110
|
return
|
|
109
111
|
end
|
|
110
112
|
self.open = false
|
|
111
|
-
self._mutex:close(
|
|
113
|
+
self._mutex:close(errors.profile_closed(self, "lock_lost"))
|
|
112
114
|
fire_hooks(self, "lock_lost", self._hooks.lock_lost)
|
|
113
115
|
fire_hooks(self, "closed", self._hooks.closed)
|
|
114
116
|
end
|
|
115
117
|
|
|
116
118
|
type SaveState = {
|
|
117
119
|
applied: boolean,
|
|
120
|
+
--- the lock found on the record (session or transaction), nil when none
|
|
121
|
+
lock: types.Lock?,
|
|
118
122
|
--- a live transaction lock is on the record; `tx_lock_expires` says until when
|
|
119
123
|
tx_locked: boolean,
|
|
120
124
|
tx_lock_expires: number,
|
|
@@ -123,6 +127,9 @@ type SaveState = {
|
|
|
123
127
|
tx_lock_lost: boolean,
|
|
124
128
|
outdated: boolean,
|
|
125
129
|
unknown_pending: types.Pending?,
|
|
130
|
+
--- the transform threw (a migration or a queued dispatcher): rethrown after
|
|
131
|
+
--- the storage call instead of being retried as a storage failure
|
|
132
|
+
failure: any,
|
|
126
133
|
}
|
|
127
134
|
|
|
128
135
|
type WriteOptions = types.LocklessWriteOptions
|
|
@@ -135,7 +142,12 @@ type WriteOptions = types.LocklessWriteOptions
|
|
|
135
142
|
--- `self._tx_lock`). `options.old_tx_lock` is the lock this write expects to
|
|
136
143
|
--- find on the record: the write only goes through (and clears that lock) when
|
|
137
144
|
--- the stored lock has the same id. If the lock is gone or belongs to someone
|
|
138
|
-
--- else, nothing is written and the call fails with
|
|
145
|
+
--- else, nothing is written and the call fails with `tx_lock_lost`.
|
|
146
|
+
---
|
|
147
|
+
--- Throws `lockless_locked` (a live session lock; the profile is closed),
|
|
148
|
+
--- `tx_lock_lost` (closed too), `outdated` (a newer server wrote the record;
|
|
149
|
+
--- nothing written), `migration_mismatch`, `roblox`, or whatever a queued
|
|
150
|
+
--- dispatcher / migration threw.
|
|
139
151
|
local function write_queued(self: LocklessProfile, options: WriteOptions)
|
|
140
152
|
local config = self.config
|
|
141
153
|
local scheduler = config._scheduler
|
|
@@ -146,23 +158,19 @@ local function write_queued(self: LocklessProfile, options: WriteOptions)
|
|
|
146
158
|
|
|
147
159
|
local state: SaveState = {
|
|
148
160
|
applied = false,
|
|
161
|
+
lock = nil,
|
|
149
162
|
tx_locked = false,
|
|
150
163
|
tx_lock_expires = 0,
|
|
151
164
|
tx_lock_lost = false,
|
|
152
165
|
session_locked = false,
|
|
153
166
|
outdated = false,
|
|
154
167
|
unknown_pending = nil,
|
|
168
|
+
failure = nil,
|
|
155
169
|
}
|
|
156
170
|
|
|
157
|
-
local function
|
|
158
|
-
state.applied = false
|
|
159
|
-
state.tx_locked = false
|
|
160
|
-
state.session_locked = false
|
|
161
|
-
state.tx_lock_lost = false
|
|
162
|
-
state.outdated = false
|
|
163
|
-
state.unknown_pending = nil
|
|
164
|
-
|
|
171
|
+
local function transform_body(old: DataRecord?): DataRecord?
|
|
165
172
|
local lock = if old then old.lock else nil
|
|
173
|
+
state.lock = lock
|
|
166
174
|
|
|
167
175
|
-- a live session lock means a locked profile owns this key now: the
|
|
168
176
|
-- profile lost its lockless standing, whatever else it expected on the
|
|
@@ -192,7 +200,7 @@ local function write_queued(self: LocklessProfile, options: WriteOptions)
|
|
|
192
200
|
|
|
193
201
|
-- more migrations applied than this server declares: a newer server
|
|
194
202
|
-- already touched the record, so this server must not write to it
|
|
195
|
-
if
|
|
203
|
+
if common.is_outdated(old, config) then
|
|
196
204
|
state.outdated = true
|
|
197
205
|
return nil
|
|
198
206
|
end
|
|
@@ -222,22 +230,45 @@ local function write_queued(self: LocklessProfile, options: WriteOptions)
|
|
|
222
230
|
}
|
|
223
231
|
end
|
|
224
232
|
|
|
233
|
+
-- the body runs migrations and queued dispatchers (user code) and may raise
|
|
234
|
+
-- a `migration_mismatch`: none of those is a storage failure, so the
|
|
235
|
+
-- storage retry must not see them. They cancel the write and are rethrown.
|
|
236
|
+
local function transform(old: DataRecord?): DataRecord?
|
|
237
|
+
state.applied = false
|
|
238
|
+
state.lock = nil
|
|
239
|
+
state.tx_locked = false
|
|
240
|
+
state.session_locked = false
|
|
241
|
+
state.tx_lock_lost = false
|
|
242
|
+
state.outdated = false
|
|
243
|
+
state.unknown_pending = nil
|
|
244
|
+
state.failure = nil
|
|
245
|
+
|
|
246
|
+
local ok, new = pcall(transform_body, old)
|
|
247
|
+
if not ok then
|
|
248
|
+
state.failure = new
|
|
249
|
+
return nil
|
|
250
|
+
end
|
|
251
|
+
return new
|
|
252
|
+
end
|
|
253
|
+
|
|
225
254
|
while true do
|
|
226
|
-
local record = utils.
|
|
227
|
-
|
|
228
|
-
|
|
255
|
+
local record = utils.update_async(config, datastore, self.key, transform, self.user_ids) :: DataRecord
|
|
256
|
+
|
|
257
|
+
if state.failure ~= nil then
|
|
258
|
+
error(state.failure, 0)
|
|
259
|
+
end
|
|
229
260
|
|
|
230
261
|
if state.session_locked then
|
|
231
262
|
self._tx_lock = nil
|
|
232
263
|
process_lock_lost(self)
|
|
233
|
-
error(
|
|
264
|
+
error(errors.lockless_locked(self, state.lock :: types.Lock), 0)
|
|
234
265
|
end
|
|
235
266
|
|
|
236
267
|
if state.tx_lock_lost then
|
|
237
268
|
-- whatever is on the record now is not ours; do not touch it again
|
|
238
269
|
self._tx_lock = nil
|
|
239
270
|
process_lock_lost(self)
|
|
240
|
-
error(
|
|
271
|
+
error(errors.tx_lock_lost(self, old_tx_lock :: types.Lock, state.lock), 0)
|
|
241
272
|
end
|
|
242
273
|
|
|
243
274
|
if state.tx_locked then
|
|
@@ -248,7 +279,8 @@ local function write_queued(self: LocklessProfile, options: WriteOptions)
|
|
|
248
279
|
|
|
249
280
|
if state.outdated then
|
|
250
281
|
error(
|
|
251
|
-
|
|
282
|
+
errors.outdated(self.key, record.migrations or {}, common.all_migration_names(config.migrations), self),
|
|
283
|
+
0
|
|
252
284
|
)
|
|
253
285
|
end
|
|
254
286
|
|
|
@@ -307,16 +339,20 @@ end
|
|
|
307
339
|
|
|
308
340
|
--- Applies every queued update to the stored data in one update_async and
|
|
309
341
|
--- refreshes the local data with the result. Marks the profile fetched.
|
|
310
|
-
---
|
|
311
|
-
---
|
|
312
|
-
---
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
342
|
+
--- Fails (keeping the queue) with `lockless_locked` if another server holds a
|
|
343
|
+
--- live session lock (the profile is closed), `outdated` if the record was
|
|
344
|
+
--- already written by a newer server, `migration_mismatch`, `roblox` or
|
|
345
|
+
--- `profile_closed`. An error thrown by a queued dispatcher propagates as is.
|
|
346
|
+
function lockless_profile.save(self: LocklessProfile): types.Result<nil>
|
|
347
|
+
return result.capture(function()
|
|
348
|
+
common.assert_open(self)
|
|
349
|
+
-- before the mutex so hooks can still queue updates that land in this save
|
|
350
|
+
fire_hooks(self, "save", self._hooks.save)
|
|
351
|
+
common.assert_open(self)
|
|
352
|
+
self._mutex:with(function()
|
|
353
|
+
write_queued(self, {})
|
|
354
|
+
end)
|
|
355
|
+
return nil
|
|
320
356
|
end)
|
|
321
357
|
end
|
|
322
358
|
|
|
@@ -326,11 +362,15 @@ function lockless_profile.unload(self: LocklessProfile)
|
|
|
326
362
|
end
|
|
327
363
|
-- last chance to queue updates while the profile is still open
|
|
328
364
|
fire_hooks(self, "closing", self._hooks.closing)
|
|
365
|
+
-- a closing hook may have unloaded the profile itself
|
|
366
|
+
if not self.open then
|
|
367
|
+
return
|
|
368
|
+
end
|
|
329
369
|
|
|
330
370
|
self.open = false
|
|
331
371
|
self._mutex:lock()
|
|
332
372
|
-- everything still waiting on the mutex (and anything after) is rejected
|
|
333
|
-
self._mutex:close(
|
|
373
|
+
self._mutex:close(errors.profile_closed(self, "unloaded"))
|
|
334
374
|
local ok, err = utils.try(function()
|
|
335
375
|
if #self.queued_updates > 0 or self.dirty_pending then
|
|
336
376
|
write_queued(self, {})
|
|
@@ -338,7 +378,9 @@ function lockless_profile.unload(self: LocklessProfile)
|
|
|
338
378
|
end)
|
|
339
379
|
self._mutex:unlock()
|
|
340
380
|
if not ok then
|
|
341
|
-
self.config._hook.warn(
|
|
381
|
+
self.config._hook.warn(
|
|
382
|
+
`datastore: failed to flush "{self.key}" on unload ({#self.queued_updates} changes lost): {tostring(err)}`
|
|
383
|
+
)
|
|
342
384
|
end
|
|
343
385
|
fire_hooks(self, "closed", self._hooks.closed)
|
|
344
386
|
end
|
|
@@ -380,14 +422,12 @@ local function read_record(self: LocklessProfile)
|
|
|
380
422
|
local config = self.config
|
|
381
423
|
local datastore = self._datastore
|
|
382
424
|
|
|
383
|
-
local record = utils.
|
|
384
|
-
return datastore.get_async(self.key)
|
|
385
|
-
end) :: DataRecord?
|
|
425
|
+
local record = utils.get_async(config, datastore, self.key) :: DataRecord?
|
|
386
426
|
|
|
387
427
|
local lock = if record then record.lock else nil
|
|
388
428
|
if lock and not lock.tx and lock.expires > config._scheduler.now() then
|
|
389
429
|
process_lock_lost(self)
|
|
390
|
-
error(
|
|
430
|
+
error(errors.lockless_locked(self, lock), 0)
|
|
391
431
|
end
|
|
392
432
|
-- an expired session lock belongs to a dead server: ignored, the next write clears it
|
|
393
433
|
-- a transaction lock does not matter for a read: the data under it is the
|
|
@@ -414,22 +454,32 @@ local function read_record(self: LocklessProfile)
|
|
|
414
454
|
return self._data
|
|
415
455
|
end
|
|
416
456
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
457
|
+
--- Fails with `lockless_locked` (a session lock is on the record; the profile
|
|
458
|
+
--- is closed), `outdated` (a newer server wrote the record), `migration_mismatch`,
|
|
459
|
+
--- `roblox`, `tx_marker_invalid` or `profile_closed`. An error thrown by a
|
|
460
|
+
--- queued dispatcher propagates as is.
|
|
461
|
+
function lockless_profile.fetch(self: LocklessProfile): types.Result<any>
|
|
462
|
+
return result.capture(function()
|
|
463
|
+
common.assert_open(self)
|
|
464
|
+
return self._mutex:with(function()
|
|
465
|
+
return read_record(self)
|
|
466
|
+
end)
|
|
421
467
|
end)
|
|
422
468
|
end
|
|
423
469
|
|
|
424
470
|
--- Queues `dispatcher` to run against the stored data on the next save. When
|
|
425
471
|
--- the profile is fetched, the local data is advanced right away so `get_data()`
|
|
426
472
|
--- reflects the queued change. A dispatcher returning `false` (or nil) is a
|
|
427
|
-
--- no-op and is not queued. Waits for any in-flight save/fetch on
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
return
|
|
432
|
-
|
|
473
|
+
--- no-op and is not queued (Ok(false)). Waits for any in-flight save/fetch on
|
|
474
|
+
--- the mutex. Fails with `profile_closed`; an error thrown by the dispatcher
|
|
475
|
+
--- propagates as is.
|
|
476
|
+
function lockless_profile.update(self: LocklessProfile, dispatcher: (any) -> any): types.Result<boolean>
|
|
477
|
+
return result.capture(function()
|
|
478
|
+
common.assert_open(self)
|
|
479
|
+
|
|
480
|
+
return self._mutex:with(function()
|
|
481
|
+
return queue_update(self, dispatcher)
|
|
482
|
+
end)
|
|
433
483
|
end)
|
|
434
484
|
end
|
|
435
485
|
|
|
@@ -449,9 +499,11 @@ local function run_flush_loop(self: LocklessProfile)
|
|
|
449
499
|
continue
|
|
450
500
|
end
|
|
451
501
|
|
|
452
|
-
local ok,
|
|
502
|
+
local ok, res = utils.try(self.save, self)
|
|
453
503
|
if not ok then
|
|
454
|
-
config._hook.warn(`datastore: failed to flush "{self.key}": {tostring(
|
|
504
|
+
config._hook.warn(`datastore: failed to flush "{self.key}": {tostring(res)}`)
|
|
505
|
+
elseif not res.success then
|
|
506
|
+
config._hook.warn(`datastore: failed to flush "{self.key}": {tostring(res.error)}`)
|
|
455
507
|
end
|
|
456
508
|
end
|
|
457
509
|
end)
|
package/src/profile/peek.luau
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
local common = require "./common"
|
|
2
|
+
local result = require "../result"
|
|
2
3
|
local types = require "../types"
|
|
3
4
|
local utils = require "../utils"
|
|
4
5
|
|
|
@@ -16,14 +17,13 @@ peek_profile.__index = peek_profile
|
|
|
16
17
|
|
|
17
18
|
--- Reads the record with `GetAsync` and adopts it: `lock` and `pending` are
|
|
18
19
|
--- taken as stored, the data gets the declared migrations applied in memory
|
|
19
|
-
--- (nothing is written). A missing key resolves to the template.
|
|
20
|
+
--- (nothing is written). A missing key resolves to the template. Throws
|
|
21
|
+
--- `roblox`, `outdated` or `migration_mismatch`.
|
|
20
22
|
local function read(self: PeekProfile): any
|
|
21
23
|
local config = self._config
|
|
22
24
|
local datastore = self._datastore
|
|
23
25
|
|
|
24
|
-
local record = utils.
|
|
25
|
-
return datastore.get_async(self.key)
|
|
26
|
-
end) :: DataRecord?
|
|
26
|
+
local record = utils.get_async(config, datastore, self.key) :: DataRecord?
|
|
27
27
|
|
|
28
28
|
self.lock = if record then record.lock else nil
|
|
29
29
|
self.pending = if record then record.pending else nil
|
|
@@ -34,12 +34,12 @@ local function read(self: PeekProfile): any
|
|
|
34
34
|
data = utils.deep_copy(config.template)
|
|
35
35
|
migrations = common.all_migration_names(config.migrations)
|
|
36
36
|
else
|
|
37
|
-
local
|
|
38
|
-
if not
|
|
39
|
-
error(
|
|
37
|
+
local migrated = common.apply_migrations(self.key, record.data, record.migrations or {}, config.migrations)
|
|
38
|
+
if not migrated.success then
|
|
39
|
+
error(migrated.err, 0)
|
|
40
40
|
end
|
|
41
|
-
data =
|
|
42
|
-
migrations =
|
|
41
|
+
data = migrated.result
|
|
42
|
+
migrations = migrated.new_migrations
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
self.migrations = migrations
|
|
@@ -51,14 +51,17 @@ function peek_profile.get_data(self: PeekProfile)
|
|
|
51
51
|
return self._data
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
--- Calls `get_async` again and replaces the snapshot. Yields.
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
--- Calls `get_async` again and replaces the snapshot. Yields. Fails with
|
|
55
|
+
--- `roblox`, `outdated` or `migration_mismatch`, leaving the previous snapshot
|
|
56
|
+
--- in place.
|
|
57
|
+
function peek_profile.refresh(self: PeekProfile): types.Result<any>
|
|
58
|
+
return result.capture(read, self)
|
|
57
59
|
end
|
|
58
60
|
|
|
59
61
|
--- Takes the first snapshot right away (yields). The profile has no mutex, no
|
|
60
|
-
--- loops and no hooks: it is a plain value that can be refreshed.
|
|
61
|
-
|
|
62
|
+
--- loops and no hooks: it is a plain value that can be refreshed. Fails with
|
|
63
|
+
--- `roblox`, `outdated` or `migration_mismatch`.
|
|
64
|
+
function peek_profile.create(options: PeekOptions): types.Result<PeekProfile>
|
|
62
65
|
local self = {} :: PeekProfile
|
|
63
66
|
self.kind = "peek"
|
|
64
67
|
self.key = options.key
|
|
@@ -70,8 +73,11 @@ function peek_profile.create(options: PeekOptions): PeekProfile
|
|
|
70
73
|
self._data = nil :: any
|
|
71
74
|
|
|
72
75
|
local profile = setmetatable(self, peek_profile) :: any
|
|
73
|
-
read
|
|
74
|
-
|
|
76
|
+
local first = result.capture(read, profile)
|
|
77
|
+
if not first.success then
|
|
78
|
+
return first
|
|
79
|
+
end
|
|
80
|
+
return result.ok(profile)
|
|
75
81
|
end
|
|
76
82
|
|
|
77
83
|
return {
|