@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PepeElToro41
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/index.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ declare namespace dataforge {
|
|
|
29
29
|
|
|
30
30
|
/** How a store reaches storage. */
|
|
31
31
|
interface Hook {
|
|
32
|
-
/** Identifies this server. Session
|
|
32
|
+
/** Identifies this server. Session lock and transaction ids are built from it. */
|
|
33
33
|
job_id: () => string;
|
|
34
34
|
/** Returns the handle for a named store. Called once per store name. */
|
|
35
35
|
get_datastore: (name: string) => DatastoreHandle;
|
|
@@ -115,16 +115,40 @@ declare namespace dataforge {
|
|
|
115
115
|
Configuration
|
|
116
116
|
*/
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
/** A named migration whose output can be checked against a target schema. */
|
|
119
|
+
interface Migration<T = unknown> {
|
|
119
120
|
name: string;
|
|
120
|
-
|
|
121
|
+
/** Stored data may have an older shape; narrow it before reading fields. */
|
|
122
|
+
apply: (data: unknown) => T;
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
/** Earlier steps may return intermediate shapes; the last must return T. */
|
|
126
|
+
type Migrations<T = unknown> = [] | [...Migration[], Migration<T>];
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Decides whether this server may work with a key at all. `data` is the
|
|
130
|
+
* raw stored data as it is in storage: `undefined` when the key does not
|
|
131
|
+
* exist yet, and with the declared migrations *not* applied. Returning
|
|
132
|
+
* `false` rejects the operation with a `predicate_failed` error: no lock
|
|
133
|
+
* is taken and nothing is written.
|
|
134
|
+
*
|
|
135
|
+
* It runs inside the `update_async` transform, so it must be pure: no
|
|
136
|
+
* yielding, no storage calls, and no mutation of `data`.
|
|
137
|
+
*/
|
|
138
|
+
type Predicate<T = unknown> = (data: T | undefined, key: string) => boolean;
|
|
139
|
+
|
|
123
140
|
interface Config<T = unknown> {
|
|
124
141
|
name: string;
|
|
125
142
|
template: T;
|
|
126
143
|
|
|
127
|
-
|
|
144
|
+
/** The final output must match the template, rather than influence its inference. */
|
|
145
|
+
migrations?: Migrations<NoInfer<T>>;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Gate on a key's stored data: every load, save, fetch and peek is
|
|
149
|
+
* rejected with `predicate_failed` when it returns `false`.
|
|
150
|
+
*/
|
|
151
|
+
predicate?: Predicate<T>;
|
|
128
152
|
|
|
129
153
|
/** seconds, default 30 (locked profiles) */
|
|
130
154
|
autosave_interval?: number;
|
|
@@ -142,8 +166,13 @@ declare namespace dataforge {
|
|
|
142
166
|
retry_base?: number;
|
|
143
167
|
}
|
|
144
168
|
|
|
145
|
-
/**
|
|
146
|
-
|
|
169
|
+
/**
|
|
170
|
+
* `Config<T>` with every optional field filled in. `predicate` stays
|
|
171
|
+
* optional: a store without one gates nothing.
|
|
172
|
+
*/
|
|
173
|
+
type ResolvedConfig<T = unknown> = Required<Omit<Config<T>, "predicate">> & {
|
|
174
|
+
predicate?: Predicate<T>;
|
|
175
|
+
};
|
|
147
176
|
|
|
148
177
|
interface TransactionConfig {
|
|
149
178
|
/**
|
|
@@ -167,6 +196,12 @@ declare namespace dataforge {
|
|
|
167
196
|
*/
|
|
168
197
|
|
|
169
198
|
interface Lock {
|
|
199
|
+
/**
|
|
200
|
+
* Identifies the *acquisition* that holds the lock, not the server:
|
|
201
|
+
* `{job_id}#{n}` for a profile's session lock, `{job_id}-{n}` for a
|
|
202
|
+
* transaction's. Two sessions of one key on the same server hold
|
|
203
|
+
* different ids, so they contend like sessions on different servers do.
|
|
204
|
+
*/
|
|
170
205
|
id: string;
|
|
171
206
|
expires: number;
|
|
172
207
|
/**
|
|
@@ -197,6 +232,188 @@ declare namespace dataforge {
|
|
|
197
232
|
|
|
198
233
|
type TxOutcome = "committed" | "aborted";
|
|
199
234
|
|
|
235
|
+
/*
|
|
236
|
+
Errors and results
|
|
237
|
+
*/
|
|
238
|
+
|
|
239
|
+
interface ErrorBase {
|
|
240
|
+
readonly type: string;
|
|
241
|
+
/** Human readable; `tostring(err)` returns it. */
|
|
242
|
+
readonly message: string;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Storage kept failing after `retry_attempts`. */
|
|
246
|
+
interface RobloxError extends ErrorBase {
|
|
247
|
+
readonly type: "roblox";
|
|
248
|
+
readonly op: "get_async" | "update_async";
|
|
249
|
+
readonly store: string;
|
|
250
|
+
readonly key: string;
|
|
251
|
+
/** The last raw error the storage hook threw. */
|
|
252
|
+
readonly cause: unknown;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** `load` / `readquire` gave up after `load_timeout`. */
|
|
256
|
+
interface TimeoutError extends ErrorBase {
|
|
257
|
+
readonly type: "timeout";
|
|
258
|
+
readonly key: string;
|
|
259
|
+
/** The last lock seen on the record (its holder). */
|
|
260
|
+
readonly lock: Lock | undefined;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** A write found the session lock is not ours; the profile is closed. */
|
|
264
|
+
interface LockLostError extends ErrorBase {
|
|
265
|
+
readonly type: "lock_lost";
|
|
266
|
+
readonly profile: Profile<any>;
|
|
267
|
+
/** The lock now on the record, `undefined` when it was wiped. */
|
|
268
|
+
readonly lock: Lock | undefined;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** The profile was released and not readquired. */
|
|
272
|
+
interface ReleasedError extends ErrorBase {
|
|
273
|
+
readonly type: "released";
|
|
274
|
+
readonly profile: Profile<any>;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Another server holds a live session lock on a lockless key; the profile is closed. */
|
|
278
|
+
interface LocklessLockedError extends ErrorBase {
|
|
279
|
+
readonly type: "lockless_locked";
|
|
280
|
+
readonly profile: LocklessProfile<any>;
|
|
281
|
+
readonly lock: Lock;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** The transaction lock a lockless participant held is gone or replaced; the profile is closed. */
|
|
285
|
+
interface TxLockLostError extends ErrorBase {
|
|
286
|
+
readonly type: "tx_lock_lost";
|
|
287
|
+
readonly profile: LocklessProfile<any>;
|
|
288
|
+
/** The transaction lock this profile held. */
|
|
289
|
+
readonly expected: Lock;
|
|
290
|
+
/** The lock now on the record, `undefined` when none. */
|
|
291
|
+
readonly lock: Lock | undefined;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* The record was written by a newer server (it lists more migrations than
|
|
296
|
+
* this store declares). Nothing is written and no lock is taken.
|
|
297
|
+
*/
|
|
298
|
+
interface OutdatedError extends ErrorBase {
|
|
299
|
+
readonly type: "outdated";
|
|
300
|
+
readonly key: string;
|
|
301
|
+
/** The profile the call ran on; `undefined` for `store.load` and `store.peek`. */
|
|
302
|
+
readonly profile: AnyProfile<any> | undefined;
|
|
303
|
+
readonly record_migrations: string[];
|
|
304
|
+
readonly declared_migrations: string[];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* `config.predicate` returned `false` for a key's stored data: this server
|
|
309
|
+
* must not work with that key. Nothing is written and no lock is taken.
|
|
310
|
+
*/
|
|
311
|
+
interface PredicateFailedError extends ErrorBase {
|
|
312
|
+
readonly type: "predicate_failed";
|
|
313
|
+
readonly key: string;
|
|
314
|
+
/** The operation the predicate rejected. */
|
|
315
|
+
readonly op: "load" | "save" | "fetch" | "peek";
|
|
316
|
+
/** The raw stored data the predicate rejected (`undefined` when the key is empty). */
|
|
317
|
+
readonly data: unknown;
|
|
318
|
+
/** The profile the call ran on; `undefined` for `store.load` and `store.peek`. */
|
|
319
|
+
readonly profile: AnyProfile<any> | undefined;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Lockless `get_data` before any fetch / flush. Thrown, never returned. */
|
|
323
|
+
interface NotFetchedError extends ErrorBase {
|
|
324
|
+
readonly type: "not_fetched";
|
|
325
|
+
readonly profile: LocklessProfile<any>;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** The profile is closed (or closed while the call waited for it). */
|
|
329
|
+
interface ProfileClosedError extends ErrorBase {
|
|
330
|
+
readonly type: "profile_closed";
|
|
331
|
+
readonly profile: AnyProfile<any>;
|
|
332
|
+
readonly reason: "unloaded" | "lock_lost";
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* A name in the record's applied migrations differs from the declared one
|
|
337
|
+
* at `index`. A record with more migrations than declared is `outdated`.
|
|
338
|
+
*/
|
|
339
|
+
interface MigrationMismatchError extends ErrorBase {
|
|
340
|
+
readonly type: "migration_mismatch";
|
|
341
|
+
readonly key: string;
|
|
342
|
+
readonly index: number;
|
|
343
|
+
readonly expected: string;
|
|
344
|
+
readonly actual: string;
|
|
345
|
+
/** Names the record has applied / this store declares. */
|
|
346
|
+
readonly applied: string[];
|
|
347
|
+
readonly declared: string[];
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** The transaction aborted. */
|
|
351
|
+
interface TxAbortedError extends ErrorBase {
|
|
352
|
+
readonly type: "tx_aborted";
|
|
353
|
+
readonly id: string;
|
|
354
|
+
/**
|
|
355
|
+
* The error of the participant that failed phase 1; `undefined` when
|
|
356
|
+
* the marker was already resolved as aborted by another server.
|
|
357
|
+
*/
|
|
358
|
+
readonly cause: DataforgeError | undefined;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** The marker datastore holds something other than "committed" / "aborted". */
|
|
362
|
+
interface TxMarkerInvalidError extends ErrorBase {
|
|
363
|
+
readonly type: "tx_marker_invalid";
|
|
364
|
+
readonly id: string;
|
|
365
|
+
readonly value: unknown;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
interface StoreClosedError extends ErrorBase {
|
|
369
|
+
readonly type: "store_closed";
|
|
370
|
+
readonly store: Store<any>;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
interface AlreadyLoadedError extends ErrorBase {
|
|
374
|
+
readonly type: "already_loaded";
|
|
375
|
+
readonly store: Store<any>;
|
|
376
|
+
readonly key: string;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Every runtime failure the library reports. Narrow on `type`. */
|
|
380
|
+
type DataforgeError =
|
|
381
|
+
| RobloxError
|
|
382
|
+
| TimeoutError
|
|
383
|
+
| LockLostError
|
|
384
|
+
| ReleasedError
|
|
385
|
+
| LocklessLockedError
|
|
386
|
+
| TxLockLostError
|
|
387
|
+
| OutdatedError
|
|
388
|
+
| PredicateFailedError
|
|
389
|
+
| NotFetchedError
|
|
390
|
+
| ProfileClosedError
|
|
391
|
+
| MigrationMismatchError
|
|
392
|
+
| TxAbortedError
|
|
393
|
+
| TxMarkerInvalidError
|
|
394
|
+
| StoreClosedError
|
|
395
|
+
| AlreadyLoadedError;
|
|
396
|
+
|
|
397
|
+
interface Ok<T> {
|
|
398
|
+
readonly success: true;
|
|
399
|
+
readonly value: T;
|
|
400
|
+
/** Returns `value`. */
|
|
401
|
+
unwrap(): T;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
interface Err {
|
|
405
|
+
readonly success: false;
|
|
406
|
+
readonly error: DataforgeError;
|
|
407
|
+
/** Throws `error` (the table itself). */
|
|
408
|
+
unwrap(): never;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* What every fallible call returns. Programmer errors (bad config, misuse
|
|
413
|
+
* of a transaction) and errors thrown by your own callbacks still throw.
|
|
414
|
+
*/
|
|
415
|
+
type Result<T> = Ok<T> | Err;
|
|
416
|
+
|
|
200
417
|
/*
|
|
201
418
|
Profiles
|
|
202
419
|
*/
|
|
@@ -216,16 +433,25 @@ declare namespace dataforge {
|
|
|
216
433
|
readonly open: boolean;
|
|
217
434
|
readonly migrations: string[];
|
|
218
435
|
|
|
219
|
-
/**
|
|
436
|
+
/**
|
|
437
|
+
* Current data. A plain accessor: throws a `DataforgeError`
|
|
438
|
+
* (`profile_closed`, or `not_fetched` for a lockless profile).
|
|
439
|
+
*/
|
|
220
440
|
get_data(): T;
|
|
221
|
-
|
|
441
|
+
/**
|
|
442
|
+
* Fails with `profile_closed`, `released`, `roblox`, `predicate_failed`,
|
|
443
|
+
* `lock_lost` (locked) or `lockless_locked`, `outdated`, ... (lockless).
|
|
444
|
+
*/
|
|
445
|
+
save(): Result<void>;
|
|
222
446
|
unload(): void;
|
|
223
447
|
/**
|
|
224
448
|
* Applies `dispatcher` to the current data. Returning `undefined` or
|
|
225
|
-
* `false` leaves the data untouched (
|
|
226
|
-
* becomes the new data and is persisted on the next save.
|
|
449
|
+
* `false` leaves the data untouched (`Ok(false)`); anything else
|
|
450
|
+
* becomes the new data and is persisted on the next save (`Ok(true)`).
|
|
451
|
+
* Fails with `profile_closed` / `released`; an error thrown by the
|
|
452
|
+
* dispatcher propagates as is.
|
|
227
453
|
*/
|
|
228
|
-
update(dispatcher: DataDispatcher<T>): boolean
|
|
454
|
+
update(dispatcher: DataDispatcher<T>): Result<boolean>;
|
|
229
455
|
|
|
230
456
|
on_change(callback: (newData: T, oldData: T | undefined) => void): void;
|
|
231
457
|
on_save(callback: () => void): void;
|
|
@@ -252,11 +478,16 @@ declare namespace dataforge {
|
|
|
252
478
|
interface Profile<T = unknown> extends ProfileBase<T> {
|
|
253
479
|
readonly kind: "locked";
|
|
254
480
|
readonly last_write: number;
|
|
481
|
+
/**
|
|
482
|
+
* The id of this acquisition's lock; re-stamped unchanged on every
|
|
483
|
+
* save, replaced by `readquire`. See `Lock.id`.
|
|
484
|
+
*/
|
|
485
|
+
readonly lock_id: string;
|
|
255
486
|
/**
|
|
256
487
|
* Whether the profile currently holds its session lock. True after
|
|
257
488
|
* `load`; `release` clears it, `readquire` takes it again. While
|
|
258
|
-
* released, `update` / `save`
|
|
259
|
-
* transaction.
|
|
489
|
+
* released, `update` / `save` fail with `released` and the profile
|
|
490
|
+
* cannot join a transaction.
|
|
260
491
|
*/
|
|
261
492
|
readonly is_locked: boolean;
|
|
262
493
|
|
|
@@ -266,13 +497,16 @@ declare namespace dataforge {
|
|
|
266
497
|
/**
|
|
267
498
|
* Writes the data and gives the session lock back, keeping the profile
|
|
268
499
|
* open so another server may take the key. No-op when already released.
|
|
500
|
+
* Fails with `profile_closed`, `roblox`, `predicate_failed` or `lock_lost`.
|
|
269
501
|
*/
|
|
270
|
-
release(): void
|
|
502
|
+
release(): Result<void>;
|
|
271
503
|
/**
|
|
272
504
|
* Takes the session lock again (waits like `load` does) and adopts the
|
|
273
|
-
* stored record as the current data. No-op when already locked.
|
|
505
|
+
* stored record as the current data. No-op when already locked. Fails
|
|
506
|
+
* with `timeout`, `profile_closed`, `roblox`, `outdated`,
|
|
507
|
+
* `predicate_failed`, `migration_mismatch`.
|
|
274
508
|
*/
|
|
275
|
-
readquire(): void
|
|
509
|
+
readquire(): Result<void>;
|
|
276
510
|
}
|
|
277
511
|
|
|
278
512
|
/** Profile without a session lock; updates are queued and folded into storage on save. */
|
|
@@ -286,9 +520,11 @@ declare namespace dataforge {
|
|
|
286
520
|
/**
|
|
287
521
|
* Reads the stored record into the local data: migrations are applied,
|
|
288
522
|
* then the queued updates replayed on top. Marks the profile fetched.
|
|
289
|
-
* Yields.
|
|
523
|
+
* Yields. Fails (keeping the queue) with `lockless_locked` if a session
|
|
524
|
+
* lock is on the record, `outdated`, `predicate_failed`, `roblox`,
|
|
525
|
+
* `migration_mismatch`, `profile_closed`.
|
|
290
526
|
*/
|
|
291
|
-
fetch(): T
|
|
527
|
+
fetch(): Result<T>;
|
|
292
528
|
}
|
|
293
529
|
|
|
294
530
|
/** Either profile kind; narrow on `kind`. */
|
|
@@ -310,8 +546,12 @@ declare namespace dataforge {
|
|
|
310
546
|
readonly migrations: string[];
|
|
311
547
|
|
|
312
548
|
get_data(): T;
|
|
313
|
-
/**
|
|
314
|
-
|
|
549
|
+
/**
|
|
550
|
+
* Calls `GetAsync` again and replaces `lock`, `pending`, `migrations`
|
|
551
|
+
* and the data. Yields. Fails with `roblox`, `predicate_failed`,
|
|
552
|
+
* `outdated` or `migration_mismatch`, keeping the previous snapshot.
|
|
553
|
+
*/
|
|
554
|
+
refresh(): Result<T>;
|
|
315
555
|
}
|
|
316
556
|
|
|
317
557
|
/*
|
|
@@ -334,37 +574,57 @@ declare namespace dataforge {
|
|
|
334
574
|
readonly profiles: ReadonlyMap<string, Profile<T>>;
|
|
335
575
|
readonly lockless_profiles: ReadonlyMap<string, LocklessProfile<T>>;
|
|
336
576
|
|
|
337
|
-
/**
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
577
|
+
/**
|
|
578
|
+
* Takes the session lock for `key` and loads it. Yields. A profile that
|
|
579
|
+
* is closing no longer counts as loaded: the load waits out its final
|
|
580
|
+
* write through the record lock instead of failing `already_loaded`.
|
|
581
|
+
*
|
|
582
|
+
* Fails with `store_closed`, `already_loaded` (a profile for `key` is
|
|
583
|
+
* still open on this store), `timeout`, `roblox`, `outdated`,
|
|
584
|
+
* `predicate_failed` (`config.predicate` rejected the stored data; no
|
|
585
|
+
* lock is taken), `migration_mismatch` or `tx_marker_invalid`.
|
|
586
|
+
*/
|
|
587
|
+
load(key: string, user_ids: number[]): Result<Profile<T>>;
|
|
588
|
+
/**
|
|
589
|
+
* Returns the profile for `key`, waiting for it to load or loading it
|
|
590
|
+
* if nobody has yet. Waiters get the same result as the load.
|
|
591
|
+
*/
|
|
592
|
+
wait_loaded(key: string, user_ids: number[]): Result<Profile<T>>;
|
|
341
593
|
get_loaded(key: string): Profile<T> | undefined;
|
|
342
594
|
|
|
343
595
|
/**
|
|
344
596
|
* Returns the lockless profile for `key`, creating it if needed. Never
|
|
345
597
|
* yields: a lockless profile does no storage work until `fetch`.
|
|
598
|
+
* Throws a `store_closed` error on a closed store.
|
|
346
599
|
*/
|
|
347
600
|
get_lockless(key: string, user_ids: number[]): LocklessProfile<T>;
|
|
348
601
|
|
|
349
602
|
/**
|
|
350
603
|
* Read-only snapshot of `key` via `GetAsync`: the current data and lock.
|
|
351
604
|
* Yields. The result is not tracked by the store, takes no lock and
|
|
352
|
-
* cannot join a transaction.
|
|
605
|
+
* cannot join a transaction. Fails with `store_closed`, `roblox`,
|
|
606
|
+
* `outdated`, `predicate_failed` or `migration_mismatch`.
|
|
353
607
|
*/
|
|
354
|
-
peek(key: string): PeekProfile<T
|
|
608
|
+
peek(key: string): Result<PeekProfile<T>>;
|
|
355
609
|
|
|
356
610
|
/**
|
|
357
611
|
* Atomically updates profiles of this store (locked or lockless).
|
|
358
612
|
* `transform` gets their data in order and returns the new data in the
|
|
359
|
-
* same order, or `false` to cancel.
|
|
360
|
-
* `true` when committed;
|
|
613
|
+
* same order, or `false` to cancel. `Ok(false)` when cancelled,
|
|
614
|
+
* `Ok(true)` when committed; fails like `dataforge.transaction`, plus
|
|
615
|
+
* `store_closed`.
|
|
361
616
|
*/
|
|
362
617
|
transaction(
|
|
363
618
|
profiles: ProfileBase<T>[],
|
|
364
619
|
transform: (data: T[]) => T[] | false,
|
|
365
620
|
config?: TransactionConfig,
|
|
366
|
-
): boolean
|
|
621
|
+
): Result<boolean>;
|
|
367
622
|
|
|
623
|
+
/**
|
|
624
|
+
* Fires for every data change of every profile of this store, including
|
|
625
|
+
* the data a key loads with (`oldData === undefined` marks that initial
|
|
626
|
+
* value).
|
|
627
|
+
*/
|
|
368
628
|
on_change(callback: (profile: AnyProfile<T>, newData: T, oldData: T | undefined) => void): void;
|
|
369
629
|
on_save(callback: (profile: AnyProfile<T>) => void): void;
|
|
370
630
|
on_closing(callback: (profile: AnyProfile<T>) => void): void;
|
|
@@ -390,14 +650,40 @@ declare namespace dataforge {
|
|
|
390
650
|
* and lockless ones. `process` reads with `ctx.get(profile)` and stages
|
|
391
651
|
* writes with `ctx.set(profile, newData)`; return `false` to cancel.
|
|
392
652
|
*
|
|
393
|
-
*
|
|
394
|
-
* `
|
|
653
|
+
* `Ok(false)` when cancelled, `Ok(true)` when committed. Fails with
|
|
654
|
+
* `tx_aborted` (see its `cause`), `profile_closed`, `released`, or what
|
|
655
|
+
* flushing a lockless participant raised. Misuse and errors thrown by
|
|
656
|
+
* `process` throw. `config` defaults its retry settings to the first
|
|
657
|
+
* profile's.
|
|
395
658
|
*/
|
|
396
659
|
function transaction(
|
|
397
660
|
profiles: ProfileBase<any>[],
|
|
398
661
|
process: (ctx: TxContext) => boolean | undefined,
|
|
399
662
|
config?: TransactionConfig,
|
|
400
|
-
): boolean
|
|
663
|
+
): Result<boolean>;
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Whether `value` is a dataforge error (what a failed `Result` carries and
|
|
667
|
+
* what the library throws internally). Lets a wrapper around library calls
|
|
668
|
+
* tell a library error from any other thrown value.
|
|
669
|
+
*/
|
|
670
|
+
function is_error(value: unknown): boolean;
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* The `Result` helpers, so you can build library-shaped results in your own
|
|
674
|
+
* wrappers. `err` only accepts a dataforge error, so a malformed result
|
|
675
|
+
* cannot be built with it.
|
|
676
|
+
*/
|
|
677
|
+
namespace result {
|
|
678
|
+
function ok<T>(value: T): Ok<T>;
|
|
679
|
+
function err(error: DataforgeError): Err;
|
|
680
|
+
/** Whether `value` is a `Result`. */
|
|
681
|
+
function is(value: unknown): boolean;
|
|
682
|
+
/** `Ok(value)` when `ok`, `Err` when `value` is a dataforge error, rethrown otherwise. */
|
|
683
|
+
function of<T>(ok: boolean, value: unknown): Result<T>;
|
|
684
|
+
/** `pcall(fn)` folded through `of`. */
|
|
685
|
+
function capture<T>(fn: () => T): Result<T>;
|
|
686
|
+
}
|
|
401
687
|
|
|
402
688
|
namespace hooks {
|
|
403
689
|
namespace memory {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rbxts/dataforge",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Roblox DataStore library with cross-key transactions, session-locked and lockless profiles, migrations and an immutable data model.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "PepeElToro41",
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
],
|
|
18
18
|
"main": "src/init.luau",
|
|
19
19
|
"types": "index.d.ts",
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test:types": "tsc --project tests/types/tsconfig.json"
|
|
22
|
+
},
|
|
20
23
|
"files": [
|
|
21
24
|
"src",
|
|
22
25
|
"index.d.ts",
|
|
@@ -27,6 +30,8 @@
|
|
|
27
30
|
"access": "public"
|
|
28
31
|
},
|
|
29
32
|
"devDependencies": {
|
|
30
|
-
"@rbxts/types": "
|
|
33
|
+
"@rbxts/compiler-types": "3.0.0-types.0",
|
|
34
|
+
"@rbxts/types": "^1.0.947",
|
|
35
|
+
"typescript": "5.5.3"
|
|
31
36
|
}
|
|
32
37
|
}
|