@evolu/common 8.10.0 → 8.12.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/Bytes.d.ts +39 -2
- package/dist/src/Bytes.d.ts.map +1 -1
- package/dist/src/Bytes.js +50 -2
- package/dist/src/Config.d.ts +22 -22
- package/dist/src/Config.d.ts.map +1 -1
- package/dist/src/Console.d.ts +62 -7
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +20 -4
- package/dist/src/Crypto.d.ts +76 -4
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +55 -4
- package/dist/src/Error.d.ts +45 -0
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +69 -0
- package/dist/src/Fs.d.ts +92 -18
- package/dist/src/Fs.d.ts.map +1 -1
- package/dist/src/Fs.js +2 -0
- package/dist/src/Identicon.d.ts +2 -2
- package/dist/src/Identicon.js +2 -2
- package/dist/src/LeakDetector.d.ts +22 -3
- package/dist/src/LeakDetector.d.ts.map +1 -1
- package/dist/src/LeakDetector.js +12 -2
- package/dist/src/LockManager.d.ts +8 -0
- package/dist/src/LockManager.d.ts.map +1 -1
- package/dist/src/LockManager.js +6 -0
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +5 -0
- package/dist/src/Platform.d.ts +47 -7
- package/dist/src/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +24 -5
- package/dist/src/Random.d.ts +25 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Random.js +14 -2
- package/dist/src/Resource.d.ts +156 -1
- package/dist/src/Resource.d.ts.map +1 -1
- package/dist/src/Resource.js +201 -72
- package/dist/src/Schedule.d.ts +11 -10
- package/dist/src/Schedule.d.ts.map +1 -1
- package/dist/src/Schedule.js +1 -1
- package/dist/src/Sqlite.d.ts +132 -16
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +63 -9
- package/dist/src/Task.d.ts +15 -4
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +41 -15
- package/dist/src/Test.d.ts +9 -0
- package/dist/src/Test.d.ts.map +1 -1
- package/dist/src/Test.js +4 -0
- package/dist/src/Time.d.ts +106 -9
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +55 -4
- package/dist/src/Type.d.ts +1455 -1310
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +1274 -517
- package/dist/src/WebSocket.d.ts +164 -13
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +133 -24
- package/dist/src/Worker.d.ts +90 -8
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +28 -2
- package/dist/src/index.d.ts +6 -7
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -3
- package/dist/src/local-first/Db.d.ts +52 -3
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +412 -137
- package/dist/src/local-first/Evolu.d.ts +412 -213
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +181 -18
- package/dist/src/local-first/Owner.d.ts +13 -30
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +13 -30
- package/dist/src/local-first/Protocol.d.ts +106 -19
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +162 -60
- package/dist/src/local-first/Query.d.ts +8 -15
- package/dist/src/local-first/Query.d.ts.map +1 -1
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +4 -2
- package/dist/src/local-first/Schema.d.ts +346 -23
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +214 -17
- package/dist/src/local-first/Shared.d.ts +537 -22
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +1437 -234
- package/dist/src/local-first/Storage.d.ts +195 -17
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +85 -22
- package/dist/src/local-first/Timestamp.d.ts +392 -41
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +403 -81
- package/dist/src/local-first/index.d.ts +0 -1
- package/dist/src/local-first/index.d.ts.map +1 -1
- package/dist/src/local-first/index.js +0 -1
- package/package.json +1 -1
- package/src/Assert.test.ts +2 -5
- package/src/Bytes.test.ts +27 -0
- package/src/Bytes.ts +58 -2
- package/src/Config.test.ts +2 -6
- package/src/Config.ts +133 -133
- package/src/Console.ts +62 -7
- package/src/Crypto.ts +76 -4
- package/src/Eq.test.ts +2 -3
- package/src/Error.test.ts +76 -3
- package/src/Error.ts +71 -0
- package/src/Fs.ts +92 -18
- package/src/Identicon.ts +2 -2
- package/src/LeakDetector.ts +22 -3
- package/src/LockManager.ts +8 -0
- package/src/Object.test.ts +27 -12
- package/src/Object.ts +5 -0
- package/src/Platform.ts +50 -8
- package/src/Random.ts +25 -2
- package/src/Resource.test.ts +837 -0
- package/src/Resource.ts +235 -15
- package/src/Schedule.test.ts +50 -12
- package/src/Schedule.ts +24 -14
- package/src/Sqlite.ts +137 -17
- package/src/Task.test.ts +189 -8
- package/src/Task.ts +56 -17
- package/src/Test.ts +9 -0
- package/src/Time.ts +106 -9
- package/src/Type.test.ts +946 -1028
- package/src/Type.ts +4195 -3136
- package/src/Types.test.ts +4 -14
- package/src/WebSocket.ts +313 -40
- package/src/Worker.ts +90 -8
- package/src/index.ts +20 -6
- package/src/local-first/Db.ts +644 -339
- package/src/local-first/Evolu.test.ts +994 -22
- package/src/local-first/Evolu.ts +625 -232
- package/src/local-first/Owner.ts +13 -30
- package/src/local-first/Protocol.test.ts +634 -10
- package/src/local-first/Protocol.ts +255 -109
- package/src/local-first/Query.ts +8 -15
- package/src/local-first/Relay.ts +4 -2
- package/src/local-first/Schema.test.ts +143 -0
- package/src/local-first/Schema.ts +376 -26
- package/src/local-first/Shared.test.ts +7731 -559
- package/src/local-first/Shared.ts +2036 -267
- package/src/local-first/Storage.ts +224 -36
- package/src/local-first/Timestamp.test.ts +344 -70
- package/src/local-first/Timestamp.ts +434 -118
- package/src/local-first/index.ts +0 -1
- package/dist/src/local-first/Error.d.ts +0 -12
- package/dist/src/local-first/Error.d.ts.map +0 -1
- package/dist/src/local-first/Error.js +0 -6
- package/dist/src/local-first/LocalAuth.d.ts +0 -150
- package/dist/src/local-first/LocalAuth.d.ts.map +0 -1
- package/dist/src/local-first/LocalAuth.js +0 -179
- package/src/local-first/Error.ts +0 -17
- package/src/local-first/LocalAuth.ts +0 -457
package/src/Resource.ts
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Concurrency-safe helpers for efficient reuse of disposable
|
|
2
|
+
* Concurrency-safe helpers for efficient reuse and replacement of disposable
|
|
3
|
+
* resources.
|
|
4
|
+
*
|
|
5
|
+
* ## Replacement policies
|
|
6
|
+
*
|
|
7
|
+
* Replacing a live resource has two policies.
|
|
8
|
+
*
|
|
9
|
+
* Exclusive replacement disposes the current resource and then creates the
|
|
10
|
+
* next, so at most one exists and there is an observable gap with none.
|
|
11
|
+
* {@link ResettableResource} implements it. Leases do not fit this policy: a
|
|
12
|
+
* reset is requested precisely when holders are stuck on a dead resource, so
|
|
13
|
+
* waiting for them inverts the purpose, and revoking them is safe only when the
|
|
14
|
+
* resource's own API is total after disposal, in which case a lease adds
|
|
15
|
+
* nothing.
|
|
16
|
+
*
|
|
17
|
+
* Overlapping replacement creates the next generation first and disposes the
|
|
18
|
+
* previous one when its last lease is released, so work in progress finishes on
|
|
19
|
+
* the generation it started with. It keeps the old generation when creation
|
|
20
|
+
* fails, at the cost of both existing for a while. It is not implemented. It
|
|
21
|
+
* belongs on {@link SharedResource} as an invalidate operation: mark the current
|
|
22
|
+
* generation stale, stop leasing it, dispose it on its last release, and create
|
|
23
|
+
* the next on the next acquire. Before that can ship,
|
|
24
|
+
* {@link SharedResourceByKey} must stop removing a key when its resource is
|
|
25
|
+
* disposed, and {@link SharedResourceByKeyWithClaims} must stop keeping one
|
|
26
|
+
* resource object per key for the lifetime of its claims.
|
|
3
27
|
*
|
|
4
28
|
* @module
|
|
5
29
|
*/
|
|
@@ -21,6 +45,8 @@ import {
|
|
|
21
45
|
sleep,
|
|
22
46
|
type AbortableFiber,
|
|
23
47
|
type DisposableRun,
|
|
48
|
+
type Fiber,
|
|
49
|
+
type Run,
|
|
24
50
|
type SemaphoreSnapshot,
|
|
25
51
|
type Task,
|
|
26
52
|
} from "./Task.ts";
|
|
@@ -46,6 +72,7 @@ import { type DistributiveOmit } from "./Types.ts";
|
|
|
46
72
|
* APIs let that error propagate as a defect. The purpose of resource helpers is
|
|
47
73
|
* to guarantee cleanup and prevent leaks.
|
|
48
74
|
*
|
|
75
|
+
* @see {@link ResettableResource}
|
|
49
76
|
* @see {@link SharedResource}
|
|
50
77
|
* @see {@link createSharedResource}
|
|
51
78
|
*/
|
|
@@ -501,6 +528,208 @@ export const createSharedResource =
|
|
|
501
528
|
return ok(sharedResource);
|
|
502
529
|
};
|
|
503
530
|
|
|
531
|
+
/**
|
|
532
|
+
* A {@link Resource} reset in place.
|
|
533
|
+
*
|
|
534
|
+
* Holds one current resource and resets it on request: the current resource is
|
|
535
|
+
* disposed first, then `create` runs again. At most one resource exists, and
|
|
536
|
+
* there is an observable gap with none. Consumers keep the ResettableResource
|
|
537
|
+
* and read the current resource through {@link ResettableResource.get | get}, so
|
|
538
|
+
* a reset needs no re-registration. A resource that cannot reset itself is the
|
|
539
|
+
* case for it. One that can, as `WebSocket.reconnect` does, keeps a stable
|
|
540
|
+
* identity and needs no wrapper.
|
|
541
|
+
*
|
|
542
|
+
* The ResettableResource keeps a stable identity while its current resource
|
|
543
|
+
* changes. {@link SharedResourceByKeyWithClaims} can retain it directly;
|
|
544
|
+
* consumers read the current resource through `get`.
|
|
545
|
+
*
|
|
546
|
+
* Repeated async disposal calls await the same cleanup and preserve any
|
|
547
|
+
* disposal failure.
|
|
548
|
+
*
|
|
549
|
+
* ### Example
|
|
550
|
+
*
|
|
551
|
+
* ```ts
|
|
552
|
+
* import {
|
|
553
|
+
* assertEqual,
|
|
554
|
+
* assertSame,
|
|
555
|
+
* createRun,
|
|
556
|
+
* createResettableResource,
|
|
557
|
+
* ok,
|
|
558
|
+
* type Task,
|
|
559
|
+
* } from "@evolu/common";
|
|
560
|
+
*
|
|
561
|
+
* interface Connection extends Disposable {
|
|
562
|
+
* readonly id: number;
|
|
563
|
+
* readonly isClosed: () => boolean;
|
|
564
|
+
* }
|
|
565
|
+
*
|
|
566
|
+
* let nextId = 1;
|
|
567
|
+
* const createConnection: Task<Connection> = () => {
|
|
568
|
+
* const id = nextId++;
|
|
569
|
+
* let isClosed = false;
|
|
570
|
+
* return ok({
|
|
571
|
+
* id,
|
|
572
|
+
* isClosed: () => isClosed,
|
|
573
|
+
* [Symbol.dispose]: () => {
|
|
574
|
+
* isClosed = true;
|
|
575
|
+
* },
|
|
576
|
+
* });
|
|
577
|
+
* };
|
|
578
|
+
*
|
|
579
|
+
* await using run = createRun();
|
|
580
|
+
* await using connection = await run.ok(
|
|
581
|
+
* createResettableResource(createConnection),
|
|
582
|
+
* );
|
|
583
|
+
* const first = connection.get();
|
|
584
|
+
* assertEqual(first?.id, 1);
|
|
585
|
+
*
|
|
586
|
+
* await run.ok(connection.reset(first));
|
|
587
|
+
* assertSame(first?.isClosed(), true);
|
|
588
|
+
* assertEqual(connection.get()?.id, 2);
|
|
589
|
+
* ```
|
|
590
|
+
*/
|
|
591
|
+
export interface ResettableResource<
|
|
592
|
+
T extends Resource,
|
|
593
|
+
> extends AsyncDisposable {
|
|
594
|
+
/**
|
|
595
|
+
* Returns the current resource, or `undefined` when there is none: while a
|
|
596
|
+
* reset is in progress, after a reset whose creation aborted, and after
|
|
597
|
+
* disposal. Callers treat every absence alike, as the resource's own
|
|
598
|
+
* not-ready state.
|
|
599
|
+
*/
|
|
600
|
+
readonly get: () => BorrowedResource<T> | undefined;
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Resets the current resource: disposes it, then creates the next one.
|
|
604
|
+
*
|
|
605
|
+
* A reset is requested because `observed` misbehaved, and it runs only while
|
|
606
|
+
* `observed` is still current or there is no current resource. A request that
|
|
607
|
+
* finds a different resource current returns without resetting, so every
|
|
608
|
+
* observer of one dead resource shares one reset, however late its request
|
|
609
|
+
* arrives. An `observed` of `undefined` is an observation of absence, as
|
|
610
|
+
* {@link ResettableResource.get | get} returns during a reset: the request
|
|
611
|
+
* runs only while nothing is current, so it retries an aborted creation and
|
|
612
|
+
* skips once a replacement exists.
|
|
613
|
+
*
|
|
614
|
+
* Resets are serialized. Owner disposal aborts a pending reset with
|
|
615
|
+
* `runDisposedAbortReason`, and a resource that `create` returns after
|
|
616
|
+
* disposal started is disposed and never becomes current.
|
|
617
|
+
*
|
|
618
|
+
* Resets follow the resource-owned {@link Run}'s lifecycle: an abort request
|
|
619
|
+
* can cancel them, and calling reset after that Run starts disposal is a
|
|
620
|
+
* programmer error. Aborting the caller's {@link Fiber} does not cancel a
|
|
621
|
+
* started reset; dependencies remain those captured at creation.
|
|
622
|
+
*/
|
|
623
|
+
readonly reset: (observed: BorrowedResource<T> | undefined) => Task<void>;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Creates {@link ResettableResource}.
|
|
628
|
+
*
|
|
629
|
+
* The first resource is created before this Task returns; if that creation
|
|
630
|
+
* aborts, nothing is left running. If the caller's Fiber aborts during initial
|
|
631
|
+
* creation, this Task waits for creation to settle, disposes any created
|
|
632
|
+
* resource, and returns the abort instead of a resource.
|
|
633
|
+
*
|
|
634
|
+
* The `create` Task must not fail: the previous resource is already disposed
|
|
635
|
+
* when it runs, so a failure would leave nothing to fall back to. Handle
|
|
636
|
+
* recoverable failures inside `create`, or return a resource whose state models
|
|
637
|
+
* them.
|
|
638
|
+
*
|
|
639
|
+
* The resource's own API must model its not-ready state, as a reconnecting
|
|
640
|
+
* connection models "connecting": `get` returns `undefined` during a reset, and
|
|
641
|
+
* callers treat that absence the same way. A resource whose API assumes
|
|
642
|
+
* readiness gains nothing here; every caller would rebuild that state around
|
|
643
|
+
* it.
|
|
644
|
+
*
|
|
645
|
+
* As with {@link createSharedResource}, a returned resource must be live and
|
|
646
|
+
* independently owned, and `create` runs on a Run created from the Run that
|
|
647
|
+
* executes this Task, so dependencies are captured at creation time.
|
|
648
|
+
*
|
|
649
|
+
* Every successful `create` invocation must return a fresh object identity,
|
|
650
|
+
* never one returned earlier. A reset skips when the observed resource is no
|
|
651
|
+
* longer current, and that comparison is by identity; a reused object makes a
|
|
652
|
+
* stale observation match the replacement and reset it again.
|
|
653
|
+
*
|
|
654
|
+
* A reset runs `create` and the resource's disposer while holding a
|
|
655
|
+
* non-reentrant lock. Neither may directly or transitively await another
|
|
656
|
+
* {@link ResettableResource.reset | reset} of this ResettableResource, because
|
|
657
|
+
* that reset waits for the same lock.
|
|
658
|
+
*/
|
|
659
|
+
export const createResettableResource =
|
|
660
|
+
<T extends Resource, D>(
|
|
661
|
+
create: Task<T, never, D>,
|
|
662
|
+
): Task<ResettableResource<T>, never, D> =>
|
|
663
|
+
async (run) => {
|
|
664
|
+
const { leakDetector } = run.deps;
|
|
665
|
+
|
|
666
|
+
let current: T | undefined;
|
|
667
|
+
|
|
668
|
+
await using disposer = new AsyncDisposableStack();
|
|
669
|
+
const resettableResourceRun = disposer.use(run.create());
|
|
670
|
+
const mutex = createMutex();
|
|
671
|
+
const resettableResourceHandle = {};
|
|
672
|
+
|
|
673
|
+
const resetFrom = (
|
|
674
|
+
observed: BorrowedResource<T> | undefined,
|
|
675
|
+
): Task<void, never, D> =>
|
|
676
|
+
mutex.withLock(async (run) => {
|
|
677
|
+
// A resource other than the observed one is current. An aborted
|
|
678
|
+
// creation leaves no current resource, so a queued request still runs.
|
|
679
|
+
if (current !== undefined && (current as unknown) !== observed) {
|
|
680
|
+
return ok();
|
|
681
|
+
}
|
|
682
|
+
const previous = current;
|
|
683
|
+
await disposeCurrent();
|
|
684
|
+
const next = await run.ok(create);
|
|
685
|
+
assert(
|
|
686
|
+
next !== previous,
|
|
687
|
+
"ResettableResource create must return a fresh object identity.",
|
|
688
|
+
);
|
|
689
|
+
// A resource returned after disposal started is disposed here, so
|
|
690
|
+
// `get` never exposes it while owner finalization runs.
|
|
691
|
+
if (run.signal.aborted) {
|
|
692
|
+
await using _next: Resource = next;
|
|
693
|
+
}
|
|
694
|
+
run.signal.throwIfAborted();
|
|
695
|
+
current = next;
|
|
696
|
+
return ok();
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
const disposeCurrent = async (): Promise<void> => {
|
|
700
|
+
await using _resource: Resource | undefined = current;
|
|
701
|
+
current = undefined;
|
|
702
|
+
};
|
|
703
|
+
resettableResourceRun.defer(() => {
|
|
704
|
+
leakDetector.untrack(resettableResourceHandle);
|
|
705
|
+
});
|
|
706
|
+
resettableResourceRun.defer(disposeCurrent);
|
|
707
|
+
|
|
708
|
+
await resettableResourceRun.ok(resetFrom(undefined));
|
|
709
|
+
// The caller can abort independently of the resource-owned Run.
|
|
710
|
+
run.signal.throwIfAborted();
|
|
711
|
+
|
|
712
|
+
const resettableResource: ResettableResource<T> = {
|
|
713
|
+
get: () => current as unknown as BorrowedResource<T> | undefined,
|
|
714
|
+
|
|
715
|
+
reset: (observed) => () => resettableResourceRun(resetFrom(observed)),
|
|
716
|
+
|
|
717
|
+
[Symbol.asyncDispose]: () => resettableResourceRun[Symbol.asyncDispose](),
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
leakDetector.track(
|
|
721
|
+
resettableResource,
|
|
722
|
+
{
|
|
723
|
+
name: "ResettableResource",
|
|
724
|
+
isLeaked: () => resettableResourceRun.getState().type === "Running",
|
|
725
|
+
},
|
|
726
|
+
resettableResourceHandle,
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
disposer.move();
|
|
730
|
+
return ok(resettableResource);
|
|
731
|
+
};
|
|
732
|
+
|
|
504
733
|
/**
|
|
505
734
|
* Shared {@link Resource}s keyed by logical identity.
|
|
506
735
|
*
|
|
@@ -895,6 +1124,11 @@ export function createSharedResourceByKey<
|
|
|
895
1124
|
* leases by key and the application does not need to associate those leases
|
|
896
1125
|
* with logical owners.
|
|
897
1126
|
*
|
|
1127
|
+
* One resource object per key is kept for the lifetime of its claims, and
|
|
1128
|
+
* callbacks receive that object. A {@link ResettableResource} can be retained
|
|
1129
|
+
* directly because its identity is stable; consumers read its current resource
|
|
1130
|
+
* through {@link ResettableResource.get | get}.
|
|
1131
|
+
*
|
|
898
1132
|
* Two accounts can share a relay connection while one also uses a local-network
|
|
899
1133
|
* transport:
|
|
900
1134
|
*
|
|
@@ -1486,17 +1720,3 @@ export const createSharedResourceByKeyWithClaims =
|
|
|
1486
1720
|
|
|
1487
1721
|
return ok(sharedResourceByKeyWithClaims);
|
|
1488
1722
|
};
|
|
1489
|
-
|
|
1490
|
-
// TODO: Add a lease-based reloadable resource when a concrete use case needs
|
|
1491
|
-
// resource swapping. A ResourceRef-style get/set API is unsafe because set can
|
|
1492
|
-
// dispose a resource still held by a caller of get. Model resource generations
|
|
1493
|
-
// explicitly and distinguish two replacement policies, probably as separate
|
|
1494
|
-
// APIs rather than a boolean option:
|
|
1495
|
-
// - Overlapping: create and validate the replacement, publish it to new leases,
|
|
1496
|
-
// then dispose the old generation after its existing leases drain. This
|
|
1497
|
-
// supports zero-downtime reload and keeps the old generation when creation
|
|
1498
|
-
// fails, but both generations temporarily exist.
|
|
1499
|
-
// - Exclusive: stop or queue new leases, drain and dispose the old generation,
|
|
1500
|
-
// then create and publish the replacement. This guarantees at most one live
|
|
1501
|
-
// resource, but introduces downtime and leaves no valid resource when creation
|
|
1502
|
-
// fails unless the owner retries or recreates the old configuration.
|
package/src/Schedule.test.ts
CHANGED
|
@@ -58,36 +58,62 @@ import {
|
|
|
58
58
|
} from "./Schedule.ts";
|
|
59
59
|
import { testCreateDeps } from "./Task.ts";
|
|
60
60
|
import {
|
|
61
|
+
type Duration,
|
|
62
|
+
durationToMillis,
|
|
61
63
|
maxMillis,
|
|
62
64
|
Millis,
|
|
65
|
+
millisToDateIso,
|
|
63
66
|
minMillis,
|
|
67
|
+
type PerformanceTime,
|
|
64
68
|
PositiveMillis,
|
|
65
69
|
testCreateTime,
|
|
66
70
|
} from "./Time.ts";
|
|
67
71
|
import { type DateIso, NonNegativeInt, Ratio } from "./Type.ts";
|
|
68
72
|
|
|
69
73
|
// Helper to create scheduleDeps with controllable time
|
|
70
|
-
const createScheduleDeps = (
|
|
74
|
+
const createScheduleDeps = () => {
|
|
71
75
|
const deps = testCreateDeps();
|
|
72
|
-
const time = testCreateTime(
|
|
76
|
+
const time = testCreateTime();
|
|
73
77
|
return { ...deps, time };
|
|
74
78
|
};
|
|
75
79
|
|
|
76
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Drives the clock schedules measure elapsed time on, including backwards, to
|
|
82
|
+
* exercise clamping a monotonic clock is not supposed to need.
|
|
83
|
+
*/
|
|
84
|
+
const createScheduleDepsWithElapsed = (...times: ReadonlyArray<number>) => {
|
|
77
85
|
const deps = testCreateDeps();
|
|
78
86
|
let index = 0;
|
|
79
|
-
const time = testCreateTime(
|
|
87
|
+
const time = testCreateTime();
|
|
88
|
+
return {
|
|
89
|
+
...deps,
|
|
90
|
+
time: {
|
|
91
|
+
...time,
|
|
92
|
+
performance: {
|
|
93
|
+
...time.performance,
|
|
94
|
+
now: () =>
|
|
95
|
+
(times[Math.min(index++, times.length - 1)] ?? 0) as PerformanceTime,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** Moves the wall clock without moving the clock schedules measure with. */
|
|
102
|
+
const createScheduleDepsWithClockJump = () => {
|
|
103
|
+
const deps = testCreateDeps();
|
|
104
|
+
const time = testCreateTime();
|
|
105
|
+
let offset = 0;
|
|
80
106
|
function now(): Millis;
|
|
81
107
|
function now(type: "DateIso"): DateIso;
|
|
82
108
|
function now(type?: "DateIso"): Millis | DateIso {
|
|
83
|
-
|
|
84
|
-
return
|
|
109
|
+
const millis = Millis.orThrow(time.now() + offset);
|
|
110
|
+
return type === "DateIso" ? millisToDateIso(millis) : millis;
|
|
85
111
|
}
|
|
86
112
|
return {
|
|
87
113
|
...deps,
|
|
88
|
-
time: {
|
|
89
|
-
|
|
90
|
-
|
|
114
|
+
time: { ...time, now },
|
|
115
|
+
jumpClock: (duration: Duration) => {
|
|
116
|
+
offset += durationToMillis(duration);
|
|
91
117
|
},
|
|
92
118
|
};
|
|
93
119
|
};
|
|
@@ -526,7 +552,7 @@ describe("elapsed", () => {
|
|
|
526
552
|
});
|
|
527
553
|
|
|
528
554
|
it("returns zero elapsed time when time moves backwards", () => {
|
|
529
|
-
const deps =
|
|
555
|
+
const deps = createScheduleDepsWithElapsed(100, 50);
|
|
530
556
|
const step = elapsed(deps);
|
|
531
557
|
|
|
532
558
|
expectOk(step(undefined), [0, 0]);
|
|
@@ -683,7 +709,7 @@ describe("maxElapsed", () => {
|
|
|
683
709
|
|
|
684
710
|
it("keeps terminal done when time moves backwards", () => {
|
|
685
711
|
const step = maxElapsed("250ms")(exponential("100ms"))(
|
|
686
|
-
|
|
712
|
+
createScheduleDepsWithElapsed(0, 250, 0),
|
|
687
713
|
);
|
|
688
714
|
|
|
689
715
|
expectOk(step(undefined), [100, 100]);
|
|
@@ -919,7 +945,7 @@ describe("compensate", () => {
|
|
|
919
945
|
});
|
|
920
946
|
|
|
921
947
|
it("keeps full delay when time moves backwards", () => {
|
|
922
|
-
const deps =
|
|
948
|
+
const deps = createScheduleDepsWithElapsed(100, 50);
|
|
923
949
|
const step = compensate(spaced("1s"))(deps);
|
|
924
950
|
|
|
925
951
|
expectOk(step(undefined), [1000, 1000]);
|
|
@@ -1059,6 +1085,18 @@ describe("resetScheduleAfter", () => {
|
|
|
1059
1085
|
expectOk(step(undefined), [200, 200]);
|
|
1060
1086
|
});
|
|
1061
1087
|
|
|
1088
|
+
it("ignores a system clock adjustment", () => {
|
|
1089
|
+
const deps = createScheduleDepsWithClockJump();
|
|
1090
|
+
const step = resetScheduleAfter("1s")(exponential("100ms"))(deps);
|
|
1091
|
+
|
|
1092
|
+
expectOk(step(undefined), [100, 100]);
|
|
1093
|
+
// An hour of wall clock passes while no time elapses.
|
|
1094
|
+
const before = deps.time.now();
|
|
1095
|
+
deps.jumpClock("1h");
|
|
1096
|
+
assertEqual(deps.time.now() - before, 3_600_000);
|
|
1097
|
+
expectOk(step(undefined), [200, 200]);
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1062
1100
|
it("keeps terminal done after inactivity", () => {
|
|
1063
1101
|
const deps = createScheduleDeps();
|
|
1064
1102
|
const step = resetScheduleAfter("1s")(take(1)(spaced("100ms")))(deps);
|
package/src/Schedule.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
durationToMillis,
|
|
24
24
|
Millis,
|
|
25
25
|
minMillis,
|
|
26
|
+
type PerformanceTime,
|
|
26
27
|
PositiveMillis,
|
|
27
28
|
saturateMillis,
|
|
28
29
|
type TimeDep,
|
|
@@ -56,6 +57,15 @@ import type { Predicate } from "./Types.ts";
|
|
|
56
57
|
* time origin on the first step call, not when `schedule(deps)` creates the
|
|
57
58
|
* step.
|
|
58
59
|
*
|
|
60
|
+
* They measure elapsed time on `Time.performance`, so a system clock adjustment
|
|
61
|
+
* cannot distort it. On platforms where that clock stops while the device
|
|
62
|
+
* sleeps, a suspended interval measures as little or no elapsed time, so a
|
|
63
|
+
* schedule does not see the gap the wall clock saw. A time box outlives the
|
|
64
|
+
* wall-clock deadline it was given, as {@link during} and {@link maxElapsed} do.
|
|
65
|
+
* {@link resetScheduleAfter} does not treat the sleep as inactivity, and
|
|
66
|
+
* {@link compensate}, {@link fixed} and {@link windowed} do not treat it as time
|
|
67
|
+
* to catch up on.
|
|
68
|
+
*
|
|
59
69
|
* ### Composing a retry policy
|
|
60
70
|
*
|
|
61
71
|
* ```ts
|
|
@@ -94,18 +104,10 @@ import type { Predicate } from "./Types.ts";
|
|
|
94
104
|
* Or use a preset:
|
|
95
105
|
*
|
|
96
106
|
* ```ts
|
|
97
|
-
* import {
|
|
98
|
-
* assertTrue,
|
|
99
|
-
* ok,
|
|
100
|
-
* retry,
|
|
101
|
-
* retryStrategyAws,
|
|
102
|
-
* type Task,
|
|
103
|
-
* } from "@evolu/common";
|
|
107
|
+
* import { ok, retry, retryStrategyAws, type Task } from "@evolu/common";
|
|
104
108
|
*
|
|
105
109
|
* const fetchData: Task<string> = () => ok("data");
|
|
106
|
-
* const
|
|
107
|
-
*
|
|
108
|
-
* assertTrue(typeof fetchWithRetry === "function");
|
|
110
|
+
* const _fetchWithRetry = retry(fetchData, retryStrategyAws);
|
|
109
111
|
* ```
|
|
110
112
|
*/
|
|
111
113
|
export type Schedule<out Output, in Input = unknown> = (
|
|
@@ -441,7 +443,15 @@ export const fixed =
|
|
|
441
443
|
/**
|
|
442
444
|
* Internal per-step metrics computed from timestamps.
|
|
443
445
|
*
|
|
444
|
-
* The schedule computes this internally from deps.time.now()
|
|
446
|
+
* The schedule computes this internally from deps.time.performance.now(), so a
|
|
447
|
+
* system clock adjustment cannot shorten or lengthen a measured elapsed time.
|
|
448
|
+
* An interval the device spent suspended can measure as no elapsed time at
|
|
449
|
+
* all.
|
|
450
|
+
*
|
|
451
|
+
* A reading that precedes the one before it is clamped to zero rather than
|
|
452
|
+
* thrown on, which is why these subtract directly instead of using
|
|
453
|
+
* `performanceDurationBetween`. A schedule measures its own progress, so a
|
|
454
|
+
* clock that misbehaves must not fail the operation being scheduled.
|
|
445
455
|
*/
|
|
446
456
|
interface ScheduleStepMetrics {
|
|
447
457
|
/** Milliseconds elapsed since the first step. */
|
|
@@ -458,11 +468,11 @@ interface ScheduleStepMetrics {
|
|
|
458
468
|
const createScheduleStepMetrics = (
|
|
459
469
|
deps: TimeDep,
|
|
460
470
|
): (() => ScheduleStepMetrics) => {
|
|
461
|
-
let start:
|
|
462
|
-
let previous:
|
|
471
|
+
let start: PerformanceTime | null = null;
|
|
472
|
+
let previous: PerformanceTime | null = null;
|
|
463
473
|
|
|
464
474
|
return () => {
|
|
465
|
-
const now = deps.time.now();
|
|
475
|
+
const now = deps.time.performance.now();
|
|
466
476
|
start ??= now;
|
|
467
477
|
const elapsed = saturateComputedMillis(now - start);
|
|
468
478
|
const elapsedSincePrevious =
|