@evolu/common 8.4.0 → 8.5.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/dist/src/Array.d.ts +24 -0
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +27 -1
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +1 -0
- package/dist/src/LeakDetector.d.ts.map +1 -1
- package/dist/src/LeakDetector.js +4 -1
- package/dist/src/Lookup.js +1 -0
- package/dist/src/Platform.js +1 -0
- package/dist/src/Relation.js +3 -3
- package/dist/src/Resource.d.ts.map +1 -1
- package/dist/src/Resource.js +9 -3
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +3 -5
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +9 -7
- package/dist/src/Store.d.ts.map +1 -1
- package/dist/src/Store.js +3 -1
- package/dist/src/String.d.ts.map +1 -1
- package/dist/src/String.js +1 -1
- package/dist/src/Task.d.ts +122 -60
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +40 -53
- package/dist/src/Test.d.ts.map +1 -1
- package/dist/src/Test.js +1 -0
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +9 -2
- package/dist/src/Type.d.ts +146 -9
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +10 -7
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +8 -0
- package/dist/src/Worker.js +1 -1
- package/dist/src/local-first/Db.js +3 -2
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +1 -0
- package/dist/src/local-first/Owner.d.ts +8 -7
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +4 -4
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +30 -24
- package/dist/src/local-first/Query.d.ts.map +1 -1
- package/dist/src/local-first/Query.js +2 -1
- package/dist/src/local-first/Relay.js +1 -1
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +4 -2
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +1 -1
- package/package.json +1 -1
- package/src/Array.ts +28 -1
- package/src/Assert.ts +1 -0
- package/src/LeakDetector.ts +4 -3
- package/src/Lookup.ts +1 -0
- package/src/Platform.ts +1 -0
- package/src/Relation.ts +3 -3
- package/src/Resource.ts +9 -3
- package/src/Result.ts +5 -5
- package/src/Sqlite.ts +9 -7
- package/src/Store.ts +3 -1
- package/src/String.ts +1 -1
- package/src/Task.ts +133 -82
- package/src/Test.ts +1 -0
- package/src/Time.ts +9 -2
- package/src/Type.ts +173 -29
- package/src/WebSocket.ts +8 -0
- package/src/Worker.ts +1 -1
- package/src/local-first/Db.ts +2 -1
- package/src/local-first/Evolu.ts +1 -0
- package/src/local-first/Owner.ts +8 -7
- package/src/local-first/Protocol.ts +40 -30
- package/src/local-first/Query.ts +6 -3
- package/src/local-first/Relay.ts +1 -1
- package/src/local-first/Shared.ts +4 -2
- package/src/local-first/Timestamp.ts +1 -1
package/src/Resource.ts
CHANGED
|
@@ -392,7 +392,7 @@ export const createSharedResource =
|
|
|
392
392
|
// only.
|
|
393
393
|
if (sharedResourceRun.getState().type !== "Running") return true;
|
|
394
394
|
|
|
395
|
-
if (idleDisposeAfter) {
|
|
395
|
+
if (idleDisposeAfter !== undefined) {
|
|
396
396
|
idleDisposeFiber = sharedResourceRun.abortable<void, never>(
|
|
397
397
|
async (run) => {
|
|
398
398
|
await run.ok(sleep(idleDisposeAfter));
|
|
@@ -414,7 +414,9 @@ export const createSharedResource =
|
|
|
414
414
|
resource: current as unknown as BorrowedResource<T>,
|
|
415
415
|
created,
|
|
416
416
|
release,
|
|
417
|
-
[Symbol.dispose]:
|
|
417
|
+
[Symbol.dispose]: () => {
|
|
418
|
+
release();
|
|
419
|
+
},
|
|
418
420
|
};
|
|
419
421
|
|
|
420
422
|
leakDetector.track(
|
|
@@ -832,6 +834,7 @@ export function createSharedResourceByKey<
|
|
|
832
834
|
using leaseDisposer = new DisposableStack();
|
|
833
835
|
const leasesByKey: Array<readonly [Lease<T>, K]> = [];
|
|
834
836
|
|
|
837
|
+
// oxlint-disable-next-line unicorn/no-useless-spread -- Snapshot keys because each acquisition awaits and can mutate the registry.
|
|
835
838
|
for (const [key] of [...sharedResourcesByKey]) {
|
|
836
839
|
const lease = await run.ok(sharedResourceByKey.acquireCurrent(key));
|
|
837
840
|
if (lease) leasesByKey.push([leaseDisposer.use(lease), key]);
|
|
@@ -1346,7 +1349,9 @@ export const createSharedResourceByKeyWithClaims =
|
|
|
1346
1349
|
const release = (): boolean => releaseClaimLease(heldClaimLease);
|
|
1347
1350
|
const claimLease: ClaimLease = {
|
|
1348
1351
|
release,
|
|
1349
|
-
[Symbol.dispose]:
|
|
1352
|
+
[Symbol.dispose]: () => {
|
|
1353
|
+
release();
|
|
1354
|
+
},
|
|
1350
1355
|
};
|
|
1351
1356
|
|
|
1352
1357
|
leakDetector.track(
|
|
@@ -1367,6 +1372,7 @@ export const createSharedResourceByKeyWithClaims =
|
|
|
1367
1372
|
resourceKey,
|
|
1368
1373
|
] of firstClaimTransitions) {
|
|
1369
1374
|
onFirstClaimAdded?.(claim, resource, resourceKey);
|
|
1375
|
+
// oxlint-disable-next-line eslint/no-loop-func -- Captures this iteration's block-scoped transition values for rollback.
|
|
1370
1376
|
compensations.defer(() => {
|
|
1371
1377
|
if (!succeeded) {
|
|
1372
1378
|
notifyLastClaimRemoved(claim, resource, resourceKey);
|
package/src/Result.ts
CHANGED
|
@@ -189,6 +189,7 @@
|
|
|
189
189
|
* @module
|
|
190
190
|
*/
|
|
191
191
|
import {
|
|
192
|
+
createMutableArray,
|
|
192
193
|
emptyArray,
|
|
193
194
|
isNonEmptyArray,
|
|
194
195
|
type NonEmptyReadonlyArray,
|
|
@@ -442,9 +443,8 @@ export const isErr = <T, E>(result: Result<T, E>): result is Err<E> =>
|
|
|
442
443
|
export const getOrThrow = <T, E>(result: Result<T, E>): T => {
|
|
443
444
|
if (result.ok) {
|
|
444
445
|
return result.value;
|
|
445
|
-
} else {
|
|
446
|
-
throw new Error("getOrThrow", { cause: result.error });
|
|
447
446
|
}
|
|
447
|
+
throw new Error("getOrThrow", { cause: result.error });
|
|
448
448
|
};
|
|
449
449
|
|
|
450
450
|
/**
|
|
@@ -572,7 +572,7 @@ export function trySync<T, E>(
|
|
|
572
572
|
export function trySync<T, E>(
|
|
573
573
|
fn: () => T,
|
|
574
574
|
mapError?: (error: unknown) => E,
|
|
575
|
-
): Result<T,
|
|
575
|
+
): Result<T, unknown> {
|
|
576
576
|
try {
|
|
577
577
|
return ok(fn());
|
|
578
578
|
} catch (error) {
|
|
@@ -644,7 +644,7 @@ export function tryAsync<T, E>(
|
|
|
644
644
|
export async function tryAsync<T, E>(
|
|
645
645
|
promiseThunk: Thunk<Awaitable<T>>,
|
|
646
646
|
mapError?: (error: unknown) => E,
|
|
647
|
-
): Promise<Result<T,
|
|
647
|
+
): Promise<Result<T, unknown>> {
|
|
648
648
|
try {
|
|
649
649
|
return ok(await promiseThunk());
|
|
650
650
|
} catch (error) {
|
|
@@ -1181,7 +1181,7 @@ export function allResult(
|
|
|
1181
1181
|
const length = input.length;
|
|
1182
1182
|
if (length === 0) return ok(emptyArray);
|
|
1183
1183
|
|
|
1184
|
-
const values =
|
|
1184
|
+
const values = createMutableArray<unknown>(length);
|
|
1185
1185
|
for (let i = 0; i < length; i++) {
|
|
1186
1186
|
const result = fn ? fn(input[i]) : (input[i] as AnyResult);
|
|
1187
1187
|
if (!result.ok) return result;
|
package/src/Sqlite.ts
CHANGED
|
@@ -256,12 +256,13 @@ export const createSqlite =
|
|
|
256
256
|
console.debug({ query });
|
|
257
257
|
|
|
258
258
|
const label =
|
|
259
|
-
query.options?.logQueryExecutionTime
|
|
260
|
-
|
|
259
|
+
query.options?.logQueryExecutionTime === true
|
|
260
|
+
? `SqliteQueryExecutionTime ${query.sql}`
|
|
261
|
+
: null;
|
|
261
262
|
|
|
262
|
-
if (label) console.time(label);
|
|
263
|
+
if (label !== null) console.time(label);
|
|
263
264
|
const result = driver.exec(query);
|
|
264
|
-
if (label) console.timeEnd(label);
|
|
265
|
+
if (label !== null) console.timeEnd(label);
|
|
265
266
|
|
|
266
267
|
if (query.options?.logExplainQueryPlan) {
|
|
267
268
|
const result = driver.exec({
|
|
@@ -343,11 +344,12 @@ const drawSqliteQueryPlan = (rows: Array<SqliteQueryPlanRow>): string =>
|
|
|
343
344
|
let indent = 0;
|
|
344
345
|
|
|
345
346
|
do {
|
|
347
|
+
// oxlint-disable-next-line eslint/no-loop-func -- find invokes the callback synchronously before parentId changes.
|
|
346
348
|
const parent = rows.find((r) => r.id === parentId);
|
|
347
349
|
if (!parent) break;
|
|
348
350
|
parentId = parent.parent;
|
|
349
351
|
indent++;
|
|
350
|
-
//
|
|
352
|
+
// oxlint-disable-next-line eslint/no-constant-condition -- A missing parent terminates the traversal above.
|
|
351
353
|
} while (true);
|
|
352
354
|
|
|
353
355
|
return `${" ".repeat(indent)}${row.detail}`;
|
|
@@ -395,7 +397,7 @@ export const createPreparedStatementsCache = <P>(
|
|
|
395
397
|
if (alwaysPrepare !== true && !query.options?.prepare)
|
|
396
398
|
return null as never;
|
|
397
399
|
let statement = statementsBySql.get(query.sql);
|
|
398
|
-
if (
|
|
400
|
+
if (statement === undefined) {
|
|
399
401
|
statement = factory(query.sql);
|
|
400
402
|
statementsBySql.set(query.sql, statement);
|
|
401
403
|
}
|
|
@@ -492,7 +494,7 @@ export const sql = (
|
|
|
492
494
|
sql.identifier = (identifier: string): SqlIdentifier => ({
|
|
493
495
|
type: "SqlIdentifier",
|
|
494
496
|
// From Kysely
|
|
495
|
-
sql: `"${identifier.
|
|
497
|
+
sql: `"${identifier.replaceAll('"', '""')}"` as SafeSql,
|
|
496
498
|
});
|
|
497
499
|
|
|
498
500
|
/**
|
package/src/Store.ts
CHANGED
|
@@ -70,7 +70,9 @@ export const createStore = <T>(initialState: T, eq?: Eq<T>): Store<T> => {
|
|
|
70
70
|
get: ref.get,
|
|
71
71
|
subscribe: (listener) => {
|
|
72
72
|
listeners.add(listener);
|
|
73
|
-
return () =>
|
|
73
|
+
return () => {
|
|
74
|
+
listeners.delete(listener);
|
|
75
|
+
};
|
|
74
76
|
},
|
|
75
77
|
|
|
76
78
|
set: (state) => {
|
package/src/String.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* pattern.
|
|
10
10
|
*/
|
|
11
11
|
export const escapeRegExp = (value: string): string =>
|
|
12
|
-
value.
|
|
12
|
+
value.replaceAll(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
13
13
|
|
|
14
14
|
/** Converts an unknown value to a compact, human-readable diagnostic string. */
|
|
15
15
|
export const safelyStringifyUnknownValue = (value: unknown): string => {
|
package/src/Task.ts
CHANGED
|
@@ -3,36 +3,34 @@
|
|
|
3
3
|
*
|
|
4
4
|
* JavaScript-native structured concurrency.
|
|
5
5
|
*
|
|
6
|
-
* Structured concurrency
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* also abort their remaining sibling branches.
|
|
6
|
+
* Structured concurrency organizes running tasks into a tree. Every child
|
|
7
|
+
* belongs to a parent, a parent waits for its children before it completes, and
|
|
8
|
+
* abort propagates from parents to descendants. Races and fail-fast control
|
|
9
|
+
* flow abort siblings that are no longer needed.
|
|
11
10
|
*
|
|
12
11
|
* With plain {@link AbortController} code, these guarantees depend on call-site
|
|
13
|
-
* discipline: someone must remember the `finally` that aborts started
|
|
14
|
-
* the await that waits for cleanup.
|
|
15
|
-
*
|
|
12
|
+
* discipline: someone must remember the `finally` that aborts started tasks and
|
|
13
|
+
* the await that waits for cleanup. Evolu makes both structural: `run(task)`
|
|
14
|
+
* registers every child before it starts, and the parent {@link Run} settles
|
|
16
15
|
* only after child cleanup finishes.
|
|
17
16
|
*
|
|
18
|
-
* Evolu
|
|
17
|
+
* Evolu implements structured concurrency with:
|
|
19
18
|
*
|
|
20
|
-
* - A {@link Task}
|
|
19
|
+
* - A {@link Task} is a function passed to Run that returns an {@link Awaitable}
|
|
20
|
+
* {@link Result} and declares its dependencies.
|
|
21
21
|
* - A {@link Run} starts Tasks and owns their lifetimes.
|
|
22
22
|
* - A {@link Fiber} is the Promise-backed handle returned when a Run starts a
|
|
23
23
|
* Task.
|
|
24
24
|
* - An {@link AbortableFiber} adds explicit abort and async disposal.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* propagation, and JavaScript resource management. Together, these primitives
|
|
29
|
-
* provide abort, cleanup, defect handling, dependency injection, monitoring,
|
|
30
|
-
* concurrency, and resource bracketing.
|
|
26
|
+
* Together, these APIs provide abort, cleanup, defect handling, dependency
|
|
27
|
+
* injection, monitoring, and resource management.
|
|
31
28
|
*
|
|
32
|
-
* Tasks return
|
|
33
|
-
* flow represented by {@link AbortError}. If a Task throws or
|
|
34
|
-
* anything else, that is a defect: the root Run reports it and
|
|
35
|
-
* tree so code does not continue in a potentially invalid
|
|
29
|
+
* Tasks return a {@link Result} containing either success or a domain error.
|
|
30
|
+
* Abort is control flow represented by {@link AbortError}. If a Task throws or
|
|
31
|
+
* rejects with anything else, that is a defect: the root Run reports it and
|
|
32
|
+
* shuts down its tree so code does not continue in a potentially invalid
|
|
33
|
+
* state.
|
|
36
34
|
*
|
|
37
35
|
* ```ts
|
|
38
36
|
* import {
|
|
@@ -201,7 +199,7 @@
|
|
|
201
199
|
*
|
|
202
200
|
* {@link fetch} with a body mode already returns a plain value, so resilience is
|
|
203
201
|
* ordinary Task composition. Combine {@link timeout} and {@link retry} to bound
|
|
204
|
-
* each attempt and retry recoverable domain
|
|
202
|
+
* each attempt and retry recoverable domain errors:
|
|
205
203
|
*
|
|
206
204
|
* ```ts
|
|
207
205
|
* import {
|
|
@@ -424,9 +422,9 @@
|
|
|
424
422
|
* expect(socketDisposed).toBe(true);
|
|
425
423
|
* ```
|
|
426
424
|
*
|
|
427
|
-
* Use {@link Run.ok} with `await using` when
|
|
428
|
-
* disposable value. Use {@link acquireUseRelease} when acquisition and
|
|
429
|
-
* are separate
|
|
425
|
+
* Use {@link Run.ok} with `await using` when a Task whose error type is `never`
|
|
426
|
+
* returns a disposable value. Use {@link acquireUseRelease} when acquisition and
|
|
427
|
+
* release are separate steps rather than a disposable value.
|
|
430
428
|
*
|
|
431
429
|
* ## Awaitable
|
|
432
430
|
*
|
|
@@ -439,7 +437,7 @@
|
|
|
439
437
|
*
|
|
440
438
|
* A Task is an async ownership boundary, not a general unit of program
|
|
441
439
|
* decomposition. Calling `run(task)` always creates a child Run by design. Use
|
|
442
|
-
*
|
|
440
|
+
* a plain async function when it does not need its own Run.
|
|
443
441
|
*
|
|
444
442
|
* A unified sync/async effect API is technically possible. It can detect
|
|
445
443
|
* Promise-like values with {@link isPromiseLike}, dispose synchronous resources
|
|
@@ -457,7 +455,7 @@
|
|
|
457
455
|
* code performs effects with the result. For example, a pure function can
|
|
458
456
|
* accept a {@link RandomNumber} value instead of depending on {@link Random}.
|
|
459
457
|
*
|
|
460
|
-
* Large CPU-bound
|
|
458
|
+
* Large CPU-bound computations, such as parsing large JSON, sorting millions of
|
|
461
459
|
* items, or complex cryptography, belong in a worker. Model the asynchronous
|
|
462
460
|
* call to that worker as a Task so Run can provide timeout, abort, cleanup, and
|
|
463
461
|
* monitoring.
|
|
@@ -517,10 +515,10 @@
|
|
|
517
515
|
* ### What should Task code do with defects?
|
|
518
516
|
*
|
|
519
517
|
* Nothing. Once a defect reaches the {@link Run}, it is too late: the root Run
|
|
520
|
-
* panics, running Tasks are aborted, and the Run tree shuts down.
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
*
|
|
518
|
+
* panics, running Tasks are aborted, and the Run tree shuts down. Use
|
|
519
|
+
* {@link trySync} or {@link tryAsync} to turn recoverable exceptions and Promise
|
|
520
|
+
* rejections into typed {@link Result} errors. Let unrecoverable failures
|
|
521
|
+
* propagate as defects.
|
|
524
522
|
*
|
|
525
523
|
* ### Why does a defect panic the whole Run tree?
|
|
526
524
|
*
|
|
@@ -619,13 +617,6 @@
|
|
|
619
617
|
* periodically await {@link yieldNow} for cooperative scheduling, and move
|
|
620
618
|
* CPU-bound work to a worker.
|
|
621
619
|
*
|
|
622
|
-
* ### Should a Task be called directly?
|
|
623
|
-
*
|
|
624
|
-
* Only inside Task internals that explicitly require same-Run execution. A
|
|
625
|
-
* direct call, `task(run)`, uses the current Run instead of creating a child
|
|
626
|
-
* Run, so it bypasses child lifetime tracking, scheduling metadata, and child
|
|
627
|
-
* disposal boundaries. Application code should use `run(task)`.
|
|
628
|
-
*
|
|
629
620
|
* ### Where are fork and join?
|
|
630
621
|
*
|
|
631
622
|
* Calling `run(task)` is fork: it starts a child Task and returns a
|
|
@@ -756,16 +747,78 @@ import type {
|
|
|
756
747
|
Predicate,
|
|
757
748
|
} from "./Types.ts";
|
|
758
749
|
|
|
759
|
-
// Core
|
|
760
|
-
|
|
761
750
|
/**
|
|
762
|
-
*
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
* Its return type is {@link Awaitable}.
|
|
751
|
+
* A function passed to {@link Run} that returns an {@link Awaitable}
|
|
752
|
+
* {@link Result} and declares its dependencies through `D`.
|
|
766
753
|
*
|
|
767
754
|
* See the {@link @evolu/common!Task | Task overview}.
|
|
768
755
|
*
|
|
756
|
+
* ### Example
|
|
757
|
+
*
|
|
758
|
+
* A Task that can't fail with a domain error:
|
|
759
|
+
*
|
|
760
|
+
* ```ts
|
|
761
|
+
* import { createRun, ok, type Task } from "@evolu/common";
|
|
762
|
+
*
|
|
763
|
+
* const greet: Task<string> = () => ok("Hello!");
|
|
764
|
+
* const main: Task<string> = async (run) => await run(greet);
|
|
765
|
+
*
|
|
766
|
+
* await using run = createRun();
|
|
767
|
+
* expectOk(await run(main), "Hello!");
|
|
768
|
+
*
|
|
769
|
+
* // Without domain errors, `run.ok` returns the Ok value.
|
|
770
|
+
* expect(await run.ok(main)).toBe("Hello!");
|
|
771
|
+
* ```
|
|
772
|
+
*
|
|
773
|
+
* A Task that can fail with a domain error:
|
|
774
|
+
*
|
|
775
|
+
* ```ts
|
|
776
|
+
* import { createRun, err, type Task, type Typed } from "@evolu/common";
|
|
777
|
+
*
|
|
778
|
+
* const findUser =
|
|
779
|
+
* (id: string): Task<string, UserNotFoundError> =>
|
|
780
|
+
* () =>
|
|
781
|
+
* err({ type: "UserNotFound", id });
|
|
782
|
+
*
|
|
783
|
+
* interface UserNotFoundError extends Typed<"UserNotFound"> {
|
|
784
|
+
* readonly id: string;
|
|
785
|
+
* }
|
|
786
|
+
*
|
|
787
|
+
* await using run = createRun();
|
|
788
|
+
* expectErr(await run(findUser("user-1")), {
|
|
789
|
+
* type: "UserNotFound",
|
|
790
|
+
* id: "user-1",
|
|
791
|
+
* });
|
|
792
|
+
* ```
|
|
793
|
+
*
|
|
794
|
+
* A Task with dependencies:
|
|
795
|
+
*
|
|
796
|
+
* ```ts
|
|
797
|
+
* import { createRun, ok, type Task } from "@evolu/common";
|
|
798
|
+
*
|
|
799
|
+
* interface Config {
|
|
800
|
+
* readonly greeting: string;
|
|
801
|
+
* }
|
|
802
|
+
*
|
|
803
|
+
* interface ConfigDep {
|
|
804
|
+
* readonly config: Config;
|
|
805
|
+
* }
|
|
806
|
+
*
|
|
807
|
+
* const greet: Task<string, never, ConfigDep> = (run) =>
|
|
808
|
+
* ok(`${run.deps.config.greeting}!`);
|
|
809
|
+
*
|
|
810
|
+
* const config: Config = { greeting: "Hello" };
|
|
811
|
+
* await using run = createRun({ config });
|
|
812
|
+
* expectOk(await run(greet), "Hello!");
|
|
813
|
+
* ```
|
|
814
|
+
*
|
|
815
|
+
* Start Tasks with `run(task)`, as shown above.
|
|
816
|
+
*
|
|
817
|
+
* A Task can also be called directly as `task(run)`, but this is rarely needed.
|
|
818
|
+
* The call executes the Task inline in the current Run, as if its body were
|
|
819
|
+
* part of the parent Task, so it does not create a child Run. This is mainly
|
|
820
|
+
* useful for Task composition helpers.
|
|
821
|
+
*
|
|
769
822
|
* @group Core
|
|
770
823
|
*/
|
|
771
824
|
export type Task<T, E = never, D = unknown> = (
|
|
@@ -937,6 +990,13 @@ export interface Run<D = unknown> {
|
|
|
937
990
|
* expectOk(userResult, "Ada");
|
|
938
991
|
* expectOk(savedResult, undefined);
|
|
939
992
|
* ```
|
|
993
|
+
*
|
|
994
|
+
* Start Tasks with `run(task)`, as shown above.
|
|
995
|
+
*
|
|
996
|
+
* A Task can also be called directly as `task(run)`, but this is rarely
|
|
997
|
+
* needed. The call executes the Task inline in the current Run, as if its
|
|
998
|
+
* body were part of the parent Task, so it does not create a child Run. This
|
|
999
|
+
* is mainly useful for Task composition helpers.
|
|
940
1000
|
*/
|
|
941
1001
|
<T, E>(task: Task<T, E, D>): Fiber<T, E, D>;
|
|
942
1002
|
|
|
@@ -1196,8 +1256,8 @@ export interface Run<D = unknown> {
|
|
|
1196
1256
|
* Creates a {@link DisposableRun} attached to the root {@link Run} with this
|
|
1197
1257
|
* Run's deps.
|
|
1198
1258
|
*
|
|
1199
|
-
* Use it
|
|
1200
|
-
*
|
|
1259
|
+
* Use it to give multiple related Tasks a shared lifetime. For a single
|
|
1260
|
+
* long-lived {@link Task}, use {@link Run.daemon}.
|
|
1201
1261
|
*
|
|
1202
1262
|
* Use deps to replace the created Run's custom deps. Default deps
|
|
1203
1263
|
* ({@link RunDefaultDeps}) are inherited unless explicitly replaced with
|
|
@@ -2520,7 +2580,6 @@ const createRunInternal = <D extends object>(
|
|
|
2520
2580
|
result = await scheduler.postTask(
|
|
2521
2581
|
() => {
|
|
2522
2582
|
startSignal.throwIfAborted();
|
|
2523
|
-
// eslint-disable-next-line evolu/no-direct-task-call -- The executor invokes the Task with its child Run.
|
|
2524
2583
|
return task(taskRun);
|
|
2525
2584
|
},
|
|
2526
2585
|
{
|
|
@@ -2530,7 +2589,6 @@ const createRunInternal = <D extends object>(
|
|
|
2530
2589
|
);
|
|
2531
2590
|
} else {
|
|
2532
2591
|
startSignal.throwIfAborted();
|
|
2533
|
-
// eslint-disable-next-line evolu/no-direct-task-call -- The executor invokes the Task with its child Run.
|
|
2534
2592
|
result = await task(taskRun);
|
|
2535
2593
|
}
|
|
2536
2594
|
|
|
@@ -2595,7 +2653,7 @@ const createRunInternal = <D extends object>(
|
|
|
2595
2653
|
getOrThrow(await run(task, taskDeps))) as Run<D>["orThrow"];
|
|
2596
2654
|
|
|
2597
2655
|
run.ok = (async (task: TaskInternal, taskDeps?: object) =>
|
|
2598
|
-
getOk((await run(task, taskDeps)) as Result<any
|
|
2656
|
+
getOk((await run(task, taskDeps)) as Result<any>)) as Run<D>["ok"];
|
|
2599
2657
|
/* eslint-enable @typescript-eslint/no-unsafe-return */
|
|
2600
2658
|
|
|
2601
2659
|
run.abortable = ((task: TaskInternal, deps?: object) =>
|
|
@@ -2724,15 +2782,12 @@ const withTaskMeta =
|
|
|
2724
2782
|
taskInternal[taskMetaSymbol]?.abortBehavior === undefined,
|
|
2725
2783
|
"abort behavior helpers cannot wrap the same Task",
|
|
2726
2784
|
);
|
|
2727
|
-
// eslint-disable-next-line evolu/no-direct-task-call -- Preserve the wrapped Task's child Run.
|
|
2728
2785
|
const wrapped: TaskInternal<T, E, D> = (run) => task(run);
|
|
2729
2786
|
const taskMeta = taskInternal[taskMetaSymbol];
|
|
2730
2787
|
wrapped[taskMetaSymbol] = taskMeta ? { ...taskMeta, ...meta } : meta;
|
|
2731
2788
|
return wrapped;
|
|
2732
2789
|
};
|
|
2733
2790
|
|
|
2734
|
-
// Task helpers
|
|
2735
|
-
|
|
2736
2791
|
/**
|
|
2737
2792
|
* A readonly record whose values are {@link Task}s.
|
|
2738
2793
|
*
|
|
@@ -2768,7 +2823,7 @@ export type InferTaskRecordDeps<TTasks extends TaskRecord> = InferTasksDeps<
|
|
|
2768
2823
|
* Options shared by {@link Task} collection helpers.
|
|
2769
2824
|
*
|
|
2770
2825
|
* `concurrency` controls how many Tasks run at once. It defaults to `1`. For
|
|
2771
|
-
* CPU-bound Tasks backed by workers or
|
|
2826
|
+
* CPU-bound Tasks backed by workers or native parallelism, a platform
|
|
2772
2827
|
* `availableParallelism()` result is often a good limit. For network or
|
|
2773
2828
|
* database Tasks, choose a limit based on the transport, server, connection
|
|
2774
2829
|
* pool, and rate limits.
|
|
@@ -2993,7 +3048,7 @@ export function all<
|
|
|
2993
3048
|
TTask extends AnyTask,
|
|
2994
3049
|
>(
|
|
2995
3050
|
values: TValues,
|
|
2996
|
-
//
|
|
3051
|
+
// Separate array and record overloads keep callback parameter inference precise.
|
|
2997
3052
|
fn: (value: TValues[keyof TValues], key: keyof TValues) => TTask,
|
|
2998
3053
|
options: AllOptions,
|
|
2999
3054
|
): Task<void, InferTaskErr<TTask>, InferTasksDeps<ReadonlyArray<TTask>>>;
|
|
@@ -3103,7 +3158,7 @@ export function all<
|
|
|
3103
3158
|
TTask extends AnyTask,
|
|
3104
3159
|
>(
|
|
3105
3160
|
values: TValues,
|
|
3106
|
-
//
|
|
3161
|
+
// Separate array and record overloads keep callback parameter inference precise.
|
|
3107
3162
|
fn: (value: TValues[keyof TValues], key: keyof TValues) => TTask,
|
|
3108
3163
|
options?: TaskCollectionOptions,
|
|
3109
3164
|
): Task<
|
|
@@ -3392,7 +3447,7 @@ export function allSettled<
|
|
|
3392
3447
|
TTask extends AnyTask,
|
|
3393
3448
|
>(
|
|
3394
3449
|
values: TValues,
|
|
3395
|
-
//
|
|
3450
|
+
// Separate array and record overloads keep callback parameter inference precise.
|
|
3396
3451
|
fn: (value: TValues[keyof TValues], key: keyof TValues) => TTask,
|
|
3397
3452
|
options?: TaskCollectionOptions,
|
|
3398
3453
|
): Task<
|
|
@@ -3499,7 +3554,7 @@ const mapInput = (
|
|
|
3499
3554
|
* This helper is a callback bridge. If `reject` forwards an Error created in a
|
|
3500
3555
|
* separate async chain, V8 cannot reconstruct the caller's zero-cost async
|
|
3501
3556
|
* stack through this bridge. Prefer native promise APIs and `await` when the
|
|
3502
|
-
* wrapped
|
|
3557
|
+
* wrapped API already returns a Promise.
|
|
3503
3558
|
*
|
|
3504
3559
|
* One-shot settlement applies only to `resolve` and `reject`. A synchronous
|
|
3505
3560
|
* throw from the setup function is a defect that panics the Run tree even after
|
|
@@ -4484,7 +4539,7 @@ export const each =
|
|
|
4484
4539
|
nextIndex += 1;
|
|
4485
4540
|
|
|
4486
4541
|
const result = await run(tasks[index]);
|
|
4487
|
-
//
|
|
4542
|
+
// stopped can flip across the await via sibling workers.
|
|
4488
4543
|
if (stopped) break;
|
|
4489
4544
|
run.signal.throwIfAborted();
|
|
4490
4545
|
const decision = onResult(result, index);
|
|
@@ -4502,6 +4557,7 @@ export const each =
|
|
|
4502
4557
|
|
|
4503
4558
|
active -= 1;
|
|
4504
4559
|
if (active === 0) wake.resolve();
|
|
4560
|
+
// oxlint-disable-next-line typescript/return-await -- StackTrace.test.ts measures this direct await edge in the worker topology.
|
|
4505
4561
|
return await parked;
|
|
4506
4562
|
};
|
|
4507
4563
|
|
|
@@ -4609,8 +4665,6 @@ export const yieldNow: Task<void> = async (run) => {
|
|
|
4609
4665
|
return ok();
|
|
4610
4666
|
};
|
|
4611
4667
|
|
|
4612
|
-
// Abortability
|
|
4613
|
-
|
|
4614
4668
|
/**
|
|
4615
4669
|
* Waits until the current {@link Run} aborts, then rejects with its
|
|
4616
4670
|
* {@link AbortError}.
|
|
@@ -4685,8 +4739,8 @@ export const waitForAbort: Task<never> = async (run) => {
|
|
|
4685
4739
|
* execution. The daemon Task continues under root Run ownership until it
|
|
4686
4740
|
* settles, observes abort, or the root Run is disposed.
|
|
4687
4741
|
*
|
|
4688
|
-
* This is not a replacement for direct {@link AbortSignal} support in
|
|
4689
|
-
*
|
|
4742
|
+
* This is not a replacement for direct {@link AbortSignal} support in APIs that
|
|
4743
|
+
* can observe abort, such as {@link fetch}, timers that accept a signal, or
|
|
4690
4744
|
* callback APIs that accept a signal. Use it as an escape hatch for Tasks that
|
|
4691
4745
|
* ignore abort when an abort request must stop waiting immediately.
|
|
4692
4746
|
*
|
|
@@ -4747,7 +4801,7 @@ export const waitForAbort: Task<never> = async (run) => {
|
|
|
4747
4801
|
* expect(finished).toBe(true);
|
|
4748
4802
|
* ```
|
|
4749
4803
|
*
|
|
4750
|
-
* Promise-
|
|
4804
|
+
* Promise-returning functions should be called inside the Task, not before it.
|
|
4751
4805
|
*
|
|
4752
4806
|
* ```ts
|
|
4753
4807
|
* import { createRun, ok, type Result, type Task } from "@evolu/common";
|
|
@@ -4856,7 +4910,7 @@ export const unabortable = /*#__PURE__*/ withTaskMeta({
|
|
|
4856
4910
|
*
|
|
4857
4911
|
* An abort request before the mask Task starts prevents entering the mask. Once
|
|
4858
4912
|
* the body starts, plain child Tasks inherit the mask, so acquire and release
|
|
4859
|
-
* can run after abort.
|
|
4913
|
+
* can run after abort. Start release Tasks directly in the original mask's
|
|
4860
4914
|
* `finally`; do not wrap release in a nested `unabortableMask`, which is a new
|
|
4861
4915
|
* critical-section entry and may not start after abort.
|
|
4862
4916
|
*
|
|
@@ -4938,7 +4992,7 @@ export const unabortableMask = <T, E, D = unknown>(
|
|
|
4938
4992
|
runInternal.abortMask > abortableMask,
|
|
4939
4993
|
"unabortableMask requires a masked Run; use run(task), not a direct call",
|
|
4940
4994
|
);
|
|
4941
|
-
const restoreToken = Symbol() as RestoreToken;
|
|
4995
|
+
const restoreToken = Symbol("restore") as RestoreToken;
|
|
4942
4996
|
|
|
4943
4997
|
// The token is local to this Task Run; descendant Runs inherit the token
|
|
4944
4998
|
// set so helpers can receive restore while the mask Task is alive.
|
|
@@ -4973,7 +5027,7 @@ export const unabortableMask = <T, E, D = unknown>(
|
|
|
4973
5027
|
* Prefer native `using`, `await using`, or {@link AsyncDisposableStack} for
|
|
4974
5028
|
* owned values that implement {@link Disposable} or {@link AsyncDisposable}. Use
|
|
4975
5029
|
* `acquireUseRelease` when acquisition must be balanced with a separate release
|
|
4976
|
-
*
|
|
5030
|
+
* step, such as unlocking, returning a pooled value, releasing a lease, or
|
|
4977
5031
|
* logging out of a session.
|
|
4978
5032
|
*
|
|
4979
5033
|
* ### Example
|
|
@@ -5045,7 +5099,7 @@ export const acquireUseRelease = <
|
|
|
5045
5099
|
if (!resourceResult.ok) return resourceResult;
|
|
5046
5100
|
|
|
5047
5101
|
try {
|
|
5048
|
-
//
|
|
5102
|
+
// oxlint-disable-next-line react/rules-of-hooks -- `use` is an acquireUseRelease callback, not a React Hook.
|
|
5049
5103
|
return await run(restore(use(resourceResult.value)));
|
|
5050
5104
|
} finally {
|
|
5051
5105
|
await run.ok(release(resourceResult.value));
|
|
@@ -5053,8 +5107,6 @@ export const acquireUseRelease = <
|
|
|
5053
5107
|
},
|
|
5054
5108
|
);
|
|
5055
5109
|
|
|
5056
|
-
// Concurrency primitives
|
|
5057
|
-
|
|
5058
5110
|
/**
|
|
5059
5111
|
* A one-shot value resolved from outside the waiting {@link Task}.
|
|
5060
5112
|
*
|
|
@@ -5230,7 +5282,6 @@ export const createGate = ({
|
|
|
5230
5282
|
|
|
5231
5283
|
return {
|
|
5232
5284
|
// Direct same-Run delegation is intentional so wait observes the current deferred.
|
|
5233
|
-
// eslint-disable-next-line evolu/no-direct-task-call
|
|
5234
5285
|
wait: (run) => deferred.task(run),
|
|
5235
5286
|
open: () => {
|
|
5236
5287
|
if (isOpen) return false;
|
|
@@ -5259,8 +5310,8 @@ export const createGate = ({
|
|
|
5259
5310
|
*
|
|
5260
5311
|
* Use {@link Semaphore.withPermit} or {@link Semaphore.withPermits} to acquire
|
|
5261
5312
|
* permits for one Task and release them when it settles. Use
|
|
5262
|
-
* {@link Semaphore.take} when
|
|
5263
|
-
*
|
|
5313
|
+
* {@link Semaphore.take} when one permit must cover several child Tasks; the
|
|
5314
|
+
* returned {@link SemaphorePermit} owns release and is disposable.
|
|
5264
5315
|
*
|
|
5265
5316
|
* Requests are not capped by the current permit count because
|
|
5266
5317
|
* {@link Semaphore.resize} can increase it later.
|
|
@@ -5362,8 +5413,8 @@ export interface SemaphorePermit extends Disposable {
|
|
|
5362
5413
|
* semaphore does not reorder requests to maximize utilization.
|
|
5363
5414
|
*
|
|
5364
5415
|
* Use `"fifo"` when fairness and predictable progress matter, such as tenant
|
|
5365
|
-
* sync, API quota, or database
|
|
5366
|
-
*
|
|
5416
|
+
* sync, API quota, or database pools where large requests must not be starved
|
|
5417
|
+
* by a stream of smaller requests.
|
|
5367
5418
|
*
|
|
5368
5419
|
* Use `"greedy"` when permits represent a shared budget and smaller or
|
|
5369
5420
|
* latency-sensitive requests should proceed around larger queued requests. For
|
|
@@ -5666,8 +5717,8 @@ export const createMutex = (): Mutex => {
|
|
|
5666
5717
|
* resources, making idle-key cleanup less predictable and making accidental key
|
|
5667
5718
|
* retention easier.
|
|
5668
5719
|
*
|
|
5669
|
-
* Use Semaphore directly when callers need to hold permits
|
|
5670
|
-
*
|
|
5720
|
+
* Use Semaphore directly when callers need to hold permits while starting
|
|
5721
|
+
* several child Tasks or resize a permit pool. Use `SemaphoreByKey` when permit
|
|
5671
5722
|
* ownership should be tied to one Task lifetime and idle keys can be forgotten
|
|
5672
5723
|
* automatically.
|
|
5673
5724
|
*
|
|
@@ -5847,9 +5898,9 @@ export function createMutexByKey<K, L = K>({
|
|
|
5847
5898
|
/**
|
|
5848
5899
|
* {@link Ref} protected by a {@link Mutex}.
|
|
5849
5900
|
*
|
|
5850
|
-
* `MutexRef` serializes reads, writes, and updates through an internal Mutex
|
|
5851
|
-
*
|
|
5852
|
-
* an update fails or is aborted, the previous value is preserved.
|
|
5901
|
+
* `MutexRef` serializes reads, writes, and updates through an internal Mutex.
|
|
5902
|
+
* Reads see a stable value, while writes and updates commit one transition at a
|
|
5903
|
+
* time. When an update fails or is aborted, the previous value is preserved.
|
|
5853
5904
|
*
|
|
5854
5905
|
* `MutexRef` is non-reentrant. Updaters and modifiers run while holding the
|
|
5855
5906
|
* internal Mutex, so calling another method on the same MutexRef from inside
|
|
@@ -5859,8 +5910,8 @@ export function createMutexByKey<K, L = K>({
|
|
|
5859
5910
|
* read-modify-write. Plain Ref cannot express that — between a sync read and a
|
|
5860
5911
|
* later write, a concurrent transition can interleave and get lost.
|
|
5861
5912
|
*
|
|
5862
|
-
* `MutexRef`
|
|
5863
|
-
* overhead. Use {@link Ref} instead for synchronous state transitions,
|
|
5913
|
+
* `MutexRef` reads, writes, and updates are Tasks and incur normal {@link Run}
|
|
5914
|
+
* lifecycle overhead. Use {@link Ref} instead for synchronous state transitions,
|
|
5864
5915
|
* especially on allocation-sensitive hot paths.
|
|
5865
5916
|
*
|
|
5866
5917
|
* @group Concurrency primitives
|
|
@@ -5981,7 +6032,7 @@ export const createMutexRef = <T>(initialValue: T): MutexRef<T> => {
|
|
|
5981
6032
|
// filtering, and pluggable log sinks.
|
|
5982
6033
|
// - Tracing spans with names, timing, parent-child relationships, attributes,
|
|
5983
6034
|
// error status, and helpers for annotating the current or child spans.
|
|
5984
|
-
// - Metrics for counters, gauges, histograms, and
|
|
6035
|
+
// - Metrics for counters, gauges, histograms, and Task execution durations.
|
|
5985
6036
|
// - Resource metadata for service name, service version, deployment
|
|
5986
6037
|
// environment, and user-provided attributes.
|
|
5987
6038
|
// - Exporters for production telemetry backends, including OTLP-compatible
|
|
@@ -5994,5 +6045,5 @@ export const createMutexRef = <T>(initialValue: T): MutexRef<T> => {
|
|
|
5994
6045
|
// - Run labels and structured annotations for rendering useful snapshot trees
|
|
5995
6046
|
// instead of anonymous ids.
|
|
5996
6047
|
// - Snapshot and trace views should preserve ownership boundaries, so reusable
|
|
5997
|
-
// resources and long-lived
|
|
5998
|
-
// unrelated child
|
|
6048
|
+
// resources and long-lived Runs appear as labeled subtrees instead of
|
|
6049
|
+
// unrelated child Runs.
|
package/src/Test.ts
CHANGED
|
@@ -50,5 +50,6 @@ export const testCreateId = (): TestCreateId => {
|
|
|
50
50
|
randomLib: testCreateRandomLib(),
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
// oxlint-disable-next-line typescript/no-unnecessary-type-arguments -- Explicit never resolves createId's conditional brand-validation rest parameter; inference rejects the argument without it.
|
|
53
54
|
return (() => createId<never>({ randomBytes })) as TestCreateId;
|
|
54
55
|
};
|