@kensio/yulin 1.21.24 → 1.21.26
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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/sdk/error/sim-sdk.error.d.ts +18 -0
- package/dist/sdk/error/sim-sdk.error.js +41 -0
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.js +1 -1
- package/dist/sdk/send-abort.d.ts +28 -0
- package/dist/sdk/send-abort.js +112 -0
- package/dist/sdk/send-patch.js +3 -1
- package/dist/service/aws/factory/sim-aws-account-region-service-builder.js +1 -0
- package/dist/service/aws/sim-aws-properties.d.ts +8 -0
- package/dist/service/aws/sim-aws.d.ts +5 -0
- package/dist/service/aws/sim-aws.js +7 -1
- package/dist/service/dynamodb/command/authorize/sim-dynamodb-authorizer.d.ts +19 -1
- package/dist/service/dynamodb/command/authorize/sim-dynamodb-authorizer.js +21 -3
- package/dist/service/dynamodb/command/authorize/sim-dynamodb-reached.d.ts +6 -3
- package/dist/service/dynamodb/command/query/sim-dynamodb-query.js +3 -1
- package/dist/service/dynamodb/command/scan/sim-dynamodb-scan.js +3 -1
- package/dist/service/dynamodb/command/table/sim-dynamodb-table-reach.d.ts +4 -0
- package/dist/service/dynamodb/command/table/sim-dynamodb-table-reach.js +1 -0
- package/dist/service/dynamodb/table/sim-dynamodb-table-arn.d.ts +13 -0
- package/dist/service/dynamodb/table/sim-dynamodb-table-arn.js +20 -0
- package/dist/service/scheduler/cfn/sim-cfn-schedule-properties.d.ts +0 -4
- package/dist/service/scheduler/cfn/sim-cfn-schedule-properties.js +2 -19
- package/dist/service/scheduler/cfn/sim-cfn-schedule-time-window.d.ts +10 -0
- package/dist/service/scheduler/cfn/sim-cfn-schedule-time-window.js +37 -0
- package/dist/service/scheduler/command/schedule/sim-scheduler-described-schedule.d.ts +1 -2
- package/dist/service/scheduler/command/schedule/sim-scheduler-described-schedule.js +2 -3
- package/dist/service/scheduler/command/schedule/sim-scheduler-schedule-writer.js +3 -0
- package/dist/service/scheduler/command/schedule/sim-scheduler-unsimulated-input.js +0 -25
- package/dist/service/scheduler/command/sim-scheduler-commands.d.ts +2 -0
- package/dist/service/scheduler/command/sim-scheduler-commands.js +1 -0
- package/dist/service/scheduler/schedule/sim-scheduler-pending-occurrences.d.ts +23 -0
- package/dist/service/scheduler/schedule/sim-scheduler-pending-occurrences.js +36 -0
- package/dist/service/scheduler/schedule/sim-scheduler-schedule.d.ts +7 -0
- package/dist/service/scheduler/schedule/sim-scheduler-schedule.js +5 -0
- package/dist/service/scheduler/schedule/sim-scheduler-schedules.d.ts +27 -1
- package/dist/service/scheduler/schedule/sim-scheduler-schedules.js +52 -8
- package/dist/service/scheduler/schedule/sim-scheduler-time-window.d.ts +33 -0
- package/dist/service/scheduler/schedule/sim-scheduler-time-window.js +93 -0
- package/dist/service/scheduler/sim-scheduler.d.ts +6 -0
- package/dist/service/scheduler/sim-scheduler.js +3 -1
- package/dist/util/random/sim-random.d.ts +35 -0
- package/dist/util/random/sim-random.js +33 -0
- package/docs/sdk/README.md +23 -0
- package/docs/services/dynamodb/README.md +26 -0
- package/docs/services/scheduler/README.md +68 -5
- package/docs/time/README.md +18 -0
- package/package.json +3 -3
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { isRecord } from "../../../util/type-guard/record.js";
|
|
2
|
+
/**
|
|
3
|
+
* Read a window length, which a template may give as a number or, through a
|
|
4
|
+
* Number Parameter, as a numeric string such as "15" or "15.0". Whether the
|
|
5
|
+
* number is a valid length is left to simulated Scheduler.
|
|
6
|
+
*/
|
|
7
|
+
function windowMinutes(value, propertyError) {
|
|
8
|
+
if (value === undefined || typeof value === "number") {
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
12
|
+
const minutes = Number(value);
|
|
13
|
+
if (Number.isFinite(minutes)) {
|
|
14
|
+
return minutes;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
throw propertyError("FlexibleTimeWindow MaximumWindowInMinutes is a number");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Read an `AWS::Scheduler::Schedule` time window into the shape
|
|
21
|
+
* CreateSchedule takes.
|
|
22
|
+
*
|
|
23
|
+
* Whether the window is valid is left to simulated Scheduler, so a template
|
|
24
|
+
* and an SDK request are refused for the same reasons.
|
|
25
|
+
*/
|
|
26
|
+
export function cfnFlexibleTimeWindow(window, propertyError) {
|
|
27
|
+
if (window === undefined) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
if (!isRecord(window)) {
|
|
31
|
+
throw propertyError("FlexibleTimeWindow is an object");
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
Mode: typeof window["Mode"] === "string" ? window["Mode"] : undefined,
|
|
35
|
+
MaximumWindowInMinutes: windowMinutes(window["MaximumWindowInMinutes"], propertyError),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -5,8 +5,7 @@ import type { SimGetScheduleCommandOutput, SimSchedulerListedSchedule } from "./
|
|
|
5
5
|
*
|
|
6
6
|
* The expression comes back as the string it was created with rather than a
|
|
7
7
|
* re-serialised version of it, so a caller comparing what they read against
|
|
8
|
-
* what they sent sees what they sent.
|
|
9
|
-
* because that is the only mode this simulation takes.
|
|
8
|
+
* what they sent sees what they sent. The same goes for `FlexibleTimeWindow`.
|
|
10
9
|
*/
|
|
11
10
|
export declare function describedSchedule(schedule: SimSchedulerSchedule): Omit<SimGetScheduleCommandOutput, "$metadata">;
|
|
12
11
|
/**
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The expression comes back as the string it was created with rather than a
|
|
5
5
|
* re-serialised version of it, so a caller comparing what they read against
|
|
6
|
-
* what they sent sees what they sent.
|
|
7
|
-
* because that is the only mode this simulation takes.
|
|
6
|
+
* what they sent sees what they sent. The same goes for `FlexibleTimeWindow`.
|
|
8
7
|
*/
|
|
9
8
|
export function describedSchedule(schedule) {
|
|
10
9
|
return {
|
|
@@ -16,7 +15,7 @@ export function describedSchedule(schedule) {
|
|
|
16
15
|
State: schedule.state.value,
|
|
17
16
|
Description: schedule.description,
|
|
18
17
|
ActionAfterCompletion: schedule.actionAfterCompletion,
|
|
19
|
-
FlexibleTimeWindow:
|
|
18
|
+
FlexibleTimeWindow: schedule.timeWindow.declared,
|
|
20
19
|
Target: {
|
|
21
20
|
Arn: schedule.target.arn.value,
|
|
22
21
|
RoleArn: schedule.target.roleArn,
|
|
@@ -2,6 +2,7 @@ import { schedulerSchedule } from "../../schedule/sim-scheduler-schedule-express
|
|
|
2
2
|
import { actionAfterCompletionIn } from "../../schedule/sim-scheduler-action-after-completion.js";
|
|
3
3
|
import { SimSchedulerSchedule } from "../../schedule/sim-scheduler-schedule.js";
|
|
4
4
|
import { SimSchedulerScheduleState } from "../../schedule/sim-scheduler-schedule-state.js";
|
|
5
|
+
import { SimSchedulerTimeWindow } from "../../schedule/sim-scheduler-time-window.js";
|
|
5
6
|
import { SimSchedulerTarget } from "../../target/sim-scheduler-target.js";
|
|
6
7
|
import { refuseUnsimulatedScheduleInput } from "./sim-scheduler-unsimulated-input.js";
|
|
7
8
|
/**
|
|
@@ -27,6 +28,7 @@ export class SimSchedulerScheduleWriter {
|
|
|
27
28
|
* replacing a schedule does not create a new one as far as AWS is concerned.
|
|
28
29
|
*/
|
|
29
30
|
write(input, requested, createdAt) {
|
|
31
|
+
const timeWindow = SimSchedulerTimeWindow.of(input.FlexibleTimeWindow);
|
|
30
32
|
refuseUnsimulatedScheduleInput(input);
|
|
31
33
|
const now = this.clock.now();
|
|
32
34
|
const schedule = new SimSchedulerSchedule({
|
|
@@ -35,6 +37,7 @@ export class SimSchedulerScheduleWriter {
|
|
|
35
37
|
accountRegionScope: this.accountRegionScope,
|
|
36
38
|
schedule: schedulerSchedule(input.ScheduleExpression, input.ScheduleExpressionTimezone),
|
|
37
39
|
timeZone: input.ScheduleExpressionTimezone,
|
|
40
|
+
timeWindow,
|
|
38
41
|
target: SimSchedulerTarget.of(input.Target),
|
|
39
42
|
state: SimSchedulerScheduleState.of(input.State),
|
|
40
43
|
actionAfterCompletion: actionAfterCompletionIn(input.ActionAfterCompletion),
|
|
@@ -18,30 +18,6 @@ function refuseUnknownTimezone(timezone) {
|
|
|
18
18
|
`as 'Europe/London', or 'UTC'`);
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
-
/**
|
|
22
|
-
* Read the flexible time window, which AWS requires on every request.
|
|
23
|
-
*
|
|
24
|
-
* `FLEXIBLE` is refused rather than treated as `OFF`. The whole point of it is
|
|
25
|
-
* that AWS invokes the target at some unpredictable moment inside the window,
|
|
26
|
-
* and a simulation firing at the exact due time instead would let a test assert
|
|
27
|
-
* on a precision real Scheduler does not offer.
|
|
28
|
-
*/
|
|
29
|
-
function refuseFlexibleWindow(input) {
|
|
30
|
-
const mode = input.FlexibleTimeWindow?.Mode;
|
|
31
|
-
if (mode === undefined) {
|
|
32
|
-
throw new SimSchedulerValidationException("FlexibleTimeWindow is required, and its Mode is OFF or FLEXIBLE");
|
|
33
|
-
}
|
|
34
|
-
if (mode === "FLEXIBLE") {
|
|
35
|
-
throw new SimSchedulerUnsimulatedInputException("FlexibleTimeWindow Mode FLEXIBLE is not simulated. Real Scheduler " +
|
|
36
|
-
"invokes the target at an unpredictable moment inside the window, " +
|
|
37
|
-
"and firing at the exact due time instead would let a test rely on " +
|
|
38
|
-
"timing AWS does not promise. Use Mode OFF.");
|
|
39
|
-
}
|
|
40
|
-
if (mode !== "OFF") {
|
|
41
|
-
throw new SimSchedulerValidationException(`Invalid parameter: FlexibleTimeWindow Mode Reason: '${mode}' is not a ` +
|
|
42
|
-
`mode. A window is OFF or FLEXIBLE.`);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
21
|
/**
|
|
46
22
|
* Refuse the schedule request inputs this simulation does not model.
|
|
47
23
|
*
|
|
@@ -50,7 +26,6 @@ function refuseFlexibleWindow(input) {
|
|
|
50
26
|
* `ScheduleExpression` and send the caller to the wrong parameter.
|
|
51
27
|
*/
|
|
52
28
|
export function refuseUnsimulatedScheduleInput(input) {
|
|
53
|
-
refuseFlexibleWindow(input);
|
|
54
29
|
refuseUnsimulatedTarget(input.Target);
|
|
55
30
|
refuseUnknownTimezone(input.ScheduleExpressionTimezone);
|
|
56
31
|
if (input.StartDate !== undefined || input.EndDate !== undefined) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BackgroundScheduler } from "../../../util/background/background.js";
|
|
2
|
+
import type { SimRandom } from "../../../util/random/sim-random.js";
|
|
2
3
|
import type { SimAwsAccountRegionScope } from "../../aws/sim-aws-account-region-scope.js";
|
|
3
4
|
import type { SimIamInterServiceAuthZ } from "../../iam/authorize/sim-iam-inter-service-auth-z.js";
|
|
4
5
|
import type { SimSchedulerDeliveryTargets } from "../delivery/sim-scheduler-delivery.js";
|
|
@@ -17,6 +18,7 @@ interface SimSchedulerCommandsProperties {
|
|
|
17
18
|
readonly background: BackgroundScheduler;
|
|
18
19
|
readonly deliveryTargets?: SimSchedulerDeliveryTargets | undefined;
|
|
19
20
|
readonly accountRegionScope: SimAwsAccountRegionScope;
|
|
21
|
+
readonly random: SimRandom;
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
22
24
|
* Every command handler one simulated Scheduler scope delegates to.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { SimSchedulerSchedule } from "./sim-scheduler-schedule.js";
|
|
2
|
+
/**
|
|
3
|
+
* The occurrences of each schedule that have fallen due and are waiting
|
|
4
|
+
* inside a flexible window to be invoked.
|
|
5
|
+
*
|
|
6
|
+
* A window longer than the gap between due instants lets occurrences overlap,
|
|
7
|
+
* and a finite schedule's last occurrence can then be invoked before an
|
|
8
|
+
* earlier one. The schedule has completed once its last occurrence has fallen
|
|
9
|
+
* due and nothing is left waiting.
|
|
10
|
+
*/
|
|
11
|
+
export declare class SimSchedulerPendingOccurrences {
|
|
12
|
+
private readonly waiting;
|
|
13
|
+
private readonly lastFallenDue;
|
|
14
|
+
/**
|
|
15
|
+
* Note an occurrence that has fallen due and will be invoked later.
|
|
16
|
+
*/
|
|
17
|
+
begin(schedule: SimSchedulerSchedule): void;
|
|
18
|
+
/**
|
|
19
|
+
* Note an occurrence that has been invoked, or given up on, and answer
|
|
20
|
+
* whether the schedule has now completed.
|
|
21
|
+
*/
|
|
22
|
+
end(schedule: SimSchedulerSchedule, last: boolean): boolean;
|
|
23
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The occurrences of each schedule that have fallen due and are waiting
|
|
3
|
+
* inside a flexible window to be invoked.
|
|
4
|
+
*
|
|
5
|
+
* A window longer than the gap between due instants lets occurrences overlap,
|
|
6
|
+
* and a finite schedule's last occurrence can then be invoked before an
|
|
7
|
+
* earlier one. The schedule has completed once its last occurrence has fallen
|
|
8
|
+
* due and nothing is left waiting.
|
|
9
|
+
*/
|
|
10
|
+
export class SimSchedulerPendingOccurrences {
|
|
11
|
+
waiting = new Map();
|
|
12
|
+
lastFallenDue = new Set();
|
|
13
|
+
/**
|
|
14
|
+
* Note an occurrence that has fallen due and will be invoked later.
|
|
15
|
+
*/
|
|
16
|
+
begin(schedule) {
|
|
17
|
+
this.waiting.set(schedule, (this.waiting.get(schedule) ?? 0) + 1);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Note an occurrence that has been invoked, or given up on, and answer
|
|
21
|
+
* whether the schedule has now completed.
|
|
22
|
+
*/
|
|
23
|
+
end(schedule, last) {
|
|
24
|
+
const remaining = (this.waiting.get(schedule) ?? 1) - 1;
|
|
25
|
+
this.waiting.set(schedule, remaining);
|
|
26
|
+
if (last) {
|
|
27
|
+
this.lastFallenDue.add(schedule);
|
|
28
|
+
}
|
|
29
|
+
if (remaining > 0 || !this.lastFallenDue.has(schedule)) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
this.waiting.delete(schedule);
|
|
33
|
+
this.lastFallenDue.delete(schedule);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { SimAwsAccountRegionScope } from "../../aws/sim-aws-account-region-
|
|
|
3
3
|
import type { SimSchedulerTarget } from "../target/sim-scheduler-target.js";
|
|
4
4
|
import type { SimSchedulerScheduleName } from "./sim-scheduler-schedule-name.js";
|
|
5
5
|
import type { SimSchedulerScheduleState } from "./sim-scheduler-schedule-state.js";
|
|
6
|
+
import type { SimSchedulerTimeWindow } from "./sim-scheduler-time-window.js";
|
|
6
7
|
/**
|
|
7
8
|
* What Scheduler does with a schedule once it has finished invoking its target.
|
|
8
9
|
*
|
|
@@ -23,6 +24,8 @@ interface SimSchedulerScheduleProperties {
|
|
|
23
24
|
readonly description?: string | undefined;
|
|
24
25
|
/** The timezone the request named, which a describe reports back. */
|
|
25
26
|
readonly timeZone?: string | undefined;
|
|
27
|
+
/** When, relative to each due time, the target is invoked. */
|
|
28
|
+
readonly timeWindow: SimSchedulerTimeWindow;
|
|
26
29
|
readonly createdAt: Date;
|
|
27
30
|
}
|
|
28
31
|
/**
|
|
@@ -60,6 +63,10 @@ export declare class SimSchedulerSchedule {
|
|
|
60
63
|
* than reporting the one it fell back to.
|
|
61
64
|
*/
|
|
62
65
|
readonly timeZone: string | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* The flexible time window each occurrence is invoked inside.
|
|
68
|
+
*/
|
|
69
|
+
readonly timeWindow: SimSchedulerTimeWindow;
|
|
63
70
|
readonly creationDate: Date;
|
|
64
71
|
/**
|
|
65
72
|
* When this schedule falls due, which is read as simulated time advances.
|
|
@@ -35,6 +35,10 @@ export class SimSchedulerSchedule {
|
|
|
35
35
|
* than reporting the one it fell back to.
|
|
36
36
|
*/
|
|
37
37
|
timeZone;
|
|
38
|
+
/**
|
|
39
|
+
* The flexible time window each occurrence is invoked inside.
|
|
40
|
+
*/
|
|
41
|
+
timeWindow;
|
|
38
42
|
creationDate;
|
|
39
43
|
/**
|
|
40
44
|
* When this schedule falls due, which is read as simulated time advances.
|
|
@@ -53,6 +57,7 @@ export class SimSchedulerSchedule {
|
|
|
53
57
|
this.actionAfterCompletion = properties.actionAfterCompletion;
|
|
54
58
|
this.description = properties.description;
|
|
55
59
|
this.timeZone = properties.timeZone;
|
|
60
|
+
this.timeWindow = properties.timeWindow;
|
|
56
61
|
this.creationDate = properties.createdAt;
|
|
57
62
|
this.modified = properties.createdAt;
|
|
58
63
|
this.held = properties.state;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BackgroundScheduler } from "../../../util/background/background.js";
|
|
2
|
+
import type { SimRandom } from "../../../util/random/sim-random.js";
|
|
2
3
|
import type { SimSchedulerTargetDelivery } from "../delivery/sim-scheduler-target-delivery.js";
|
|
3
4
|
import type { SimSchedulerSchedule } from "./sim-scheduler-schedule.js";
|
|
4
5
|
import type { SimSchedulerScheduleStore } from "./sim-scheduler-schedule-store.js";
|
|
@@ -6,6 +7,8 @@ interface SimSchedulerSchedulesProperties {
|
|
|
6
7
|
readonly schedules: SimSchedulerScheduleStore;
|
|
7
8
|
readonly delivery: SimSchedulerTargetDelivery;
|
|
8
9
|
readonly background: BackgroundScheduler;
|
|
10
|
+
/** Where each occurrence's moment inside a flexible window is drawn. */
|
|
11
|
+
readonly random: SimRandom;
|
|
9
12
|
}
|
|
10
13
|
/**
|
|
11
14
|
* What makes a schedule fire, which is simulated time reaching it.
|
|
@@ -22,11 +25,19 @@ interface SimSchedulerSchedulesProperties {
|
|
|
22
25
|
* no timer to cancel, only a firing that finds itself out of date. That is also
|
|
23
26
|
* what makes an update reschedule from the new expression rather than keeping
|
|
24
27
|
* the old due times, since an update stores a new schedule and arms it.
|
|
28
|
+
*
|
|
29
|
+
* A schedule with a flexible time window is armed for each due instant all
|
|
30
|
+
* the same, and then invokes its target at a moment drawn inside the window.
|
|
31
|
+
* The next occurrence is armed from the due instant rather than from the
|
|
32
|
+
* invocation, so the window delays each invocation without moving the
|
|
33
|
+
* schedule.
|
|
25
34
|
*/
|
|
26
35
|
export declare class SimSchedulerSchedules {
|
|
27
36
|
private readonly schedules;
|
|
28
37
|
private readonly delivery;
|
|
29
38
|
private readonly background;
|
|
39
|
+
private readonly random;
|
|
40
|
+
private readonly pending;
|
|
30
41
|
constructor(properties: SimSchedulerSchedulesProperties);
|
|
31
42
|
/**
|
|
32
43
|
* Start a schedule, from now.
|
|
@@ -40,9 +51,24 @@ export declare class SimSchedulerSchedules {
|
|
|
40
51
|
*/
|
|
41
52
|
private armAfter;
|
|
42
53
|
/**
|
|
43
|
-
*
|
|
54
|
+
* Handle a schedule that has fallen due: arm the next occurrence, and
|
|
55
|
+
* invoke the target at the moment its time window gives this one.
|
|
56
|
+
*/
|
|
57
|
+
private occur;
|
|
58
|
+
/**
|
|
59
|
+
* Invoke a schedule's target for one occurrence.
|
|
60
|
+
*
|
|
61
|
+
* A schedule deleted or replaced while its occurrence waited inside the
|
|
62
|
+
* window is not invoked. The delivery carries the due instant rather than
|
|
63
|
+
* the moment it was made, since that is the occurrence it belongs to. A
|
|
64
|
+
* schedule completes with whichever occurrence is invoked last, which need
|
|
65
|
+
* not be its last due instant when windows overlap.
|
|
44
66
|
*/
|
|
45
67
|
private fire;
|
|
68
|
+
/**
|
|
69
|
+
* Whether a schedule has been deleted or replaced since it was armed.
|
|
70
|
+
*/
|
|
71
|
+
private isStale;
|
|
46
72
|
/**
|
|
47
73
|
* Deal with a schedule that has no next occurrence.
|
|
48
74
|
*
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SimSchedulerPendingOccurrences } from "./sim-scheduler-pending-occurrences.js";
|
|
1
2
|
/**
|
|
2
3
|
* What makes a schedule fire, which is simulated time reaching it.
|
|
3
4
|
*
|
|
@@ -13,15 +14,24 @@
|
|
|
13
14
|
* no timer to cancel, only a firing that finds itself out of date. That is also
|
|
14
15
|
* what makes an update reschedule from the new expression rather than keeping
|
|
15
16
|
* the old due times, since an update stores a new schedule and arms it.
|
|
17
|
+
*
|
|
18
|
+
* A schedule with a flexible time window is armed for each due instant all
|
|
19
|
+
* the same, and then invokes its target at a moment drawn inside the window.
|
|
20
|
+
* The next occurrence is armed from the due instant rather than from the
|
|
21
|
+
* invocation, so the window delays each invocation without moving the
|
|
22
|
+
* schedule.
|
|
16
23
|
*/
|
|
17
24
|
export class SimSchedulerSchedules {
|
|
18
25
|
schedules;
|
|
19
26
|
delivery;
|
|
20
27
|
background;
|
|
28
|
+
random;
|
|
29
|
+
pending = new SimSchedulerPendingOccurrences();
|
|
21
30
|
constructor(properties) {
|
|
22
31
|
this.schedules = properties.schedules;
|
|
23
32
|
this.delivery = properties.delivery;
|
|
24
33
|
this.background = properties.background;
|
|
34
|
+
this.random = properties.random;
|
|
25
35
|
}
|
|
26
36
|
/**
|
|
27
37
|
* Start a schedule, from now.
|
|
@@ -41,15 +51,45 @@ export class SimSchedulerSchedules {
|
|
|
41
51
|
return;
|
|
42
52
|
}
|
|
43
53
|
this.background.scheduleAt(due, () => {
|
|
44
|
-
this.
|
|
54
|
+
this.occur(schedule, due);
|
|
55
|
+
return Promise.resolve();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Handle a schedule that has fallen due: arm the next occurrence, and
|
|
60
|
+
* invoke the target at the moment its time window gives this one.
|
|
61
|
+
*/
|
|
62
|
+
occur(schedule, due) {
|
|
63
|
+
if (this.isStale(schedule)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const last = schedule.schedule.nextAfter(due) === undefined;
|
|
67
|
+
if (!last) {
|
|
68
|
+
this.armAfter(schedule, due);
|
|
69
|
+
}
|
|
70
|
+
const at = schedule.timeWindow.invocationAt(due, this.random);
|
|
71
|
+
this.pending.begin(schedule);
|
|
72
|
+
if (at.getTime() === due.getTime()) {
|
|
73
|
+
this.fire(schedule, due, last);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.background.scheduleAt(at, () => {
|
|
77
|
+
this.fire(schedule, due, last);
|
|
45
78
|
return Promise.resolve();
|
|
46
79
|
});
|
|
47
80
|
}
|
|
48
81
|
/**
|
|
49
|
-
*
|
|
82
|
+
* Invoke a schedule's target for one occurrence.
|
|
83
|
+
*
|
|
84
|
+
* A schedule deleted or replaced while its occurrence waited inside the
|
|
85
|
+
* window is not invoked. The delivery carries the due instant rather than
|
|
86
|
+
* the moment it was made, since that is the occurrence it belongs to. A
|
|
87
|
+
* schedule completes with whichever occurrence is invoked last, which need
|
|
88
|
+
* not be its last due instant when windows overlap.
|
|
50
89
|
*/
|
|
51
|
-
fire(schedule, due) {
|
|
52
|
-
|
|
90
|
+
fire(schedule, due, last) {
|
|
91
|
+
const completed = this.pending.end(schedule, last);
|
|
92
|
+
if (this.isStale(schedule)) {
|
|
53
93
|
return;
|
|
54
94
|
}
|
|
55
95
|
const invoked = schedule.state.isEnabled;
|
|
@@ -58,11 +98,15 @@ export class SimSchedulerSchedules {
|
|
|
58
98
|
await this.delivery.deliver({ schedule, at: due });
|
|
59
99
|
});
|
|
60
100
|
}
|
|
61
|
-
if (
|
|
62
|
-
this.
|
|
63
|
-
return;
|
|
101
|
+
if (completed) {
|
|
102
|
+
this.completed(schedule, invoked);
|
|
64
103
|
}
|
|
65
|
-
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Whether a schedule has been deleted or replaced since it was armed.
|
|
107
|
+
*/
|
|
108
|
+
isStale(schedule) {
|
|
109
|
+
return (this.schedules.find(schedule.groupName, schedule.name.value) !== schedule);
|
|
66
110
|
}
|
|
67
111
|
/**
|
|
68
112
|
* Deal with a schedule that has no next occurrence.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SimRandom } from "../../../util/random/sim-random.js";
|
|
2
|
+
import type { SimSchedulerFlexibleTimeWindow } from "../command/schedule/schedule.command.js";
|
|
3
|
+
/**
|
|
4
|
+
* When, relative to its due time, a schedule invokes its target.
|
|
5
|
+
*
|
|
6
|
+
* With `OFF` that is the due time itself. With `FLEXIBLE` it is some moment
|
|
7
|
+
* after the due time and inside the window, which AWS chooses and does not
|
|
8
|
+
* reveal. Firing on the dot instead would let a test pass by relying on
|
|
9
|
+
* timing AWS does not promise, so each occurrence draws its own moment.
|
|
10
|
+
*/
|
|
11
|
+
export declare class SimSchedulerTimeWindow {
|
|
12
|
+
/**
|
|
13
|
+
* The window length in minutes, or nothing when the mode is `OFF`.
|
|
14
|
+
*/
|
|
15
|
+
readonly minutes: number | undefined;
|
|
16
|
+
private constructor();
|
|
17
|
+
/**
|
|
18
|
+
* Read the window a request carries, which AWS requires on every one.
|
|
19
|
+
*/
|
|
20
|
+
static of(window: SimSchedulerFlexibleTimeWindow | undefined): SimSchedulerTimeWindow;
|
|
21
|
+
/**
|
|
22
|
+
* The window as GetSchedule reports it, which is what the request sent.
|
|
23
|
+
*/
|
|
24
|
+
get declared(): SimSchedulerFlexibleTimeWindow;
|
|
25
|
+
/**
|
|
26
|
+
* The instant one occurrence invokes its target, drawn from `random`.
|
|
27
|
+
*
|
|
28
|
+
* It is never before the due time and always before the window closes, so
|
|
29
|
+
* a test that moves the clock past the end of the window sees the
|
|
30
|
+
* invocation whatever was drawn.
|
|
31
|
+
*/
|
|
32
|
+
invocationAt(due: Date, random: SimRandom): Date;
|
|
33
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { SimSchedulerValidationException } from "../error/sim-scheduler.error.js";
|
|
2
|
+
const minimumWindowInMinutes = 1;
|
|
3
|
+
const maximumWindowInMinutes = 1440;
|
|
4
|
+
const millisecondsPerMinute = 60_000;
|
|
5
|
+
/**
|
|
6
|
+
* Refuse a value the API model's own constraints reject, in the words AWS
|
|
7
|
+
* uses for them.
|
|
8
|
+
*/
|
|
9
|
+
function constraintFailure(value, member, constraint) {
|
|
10
|
+
return new SimSchedulerValidationException(`1 validation error detected: Value '${value}' at ` +
|
|
11
|
+
`'flexibleTimeWindow.${member}' failed to satisfy constraint: ` +
|
|
12
|
+
`Member must ${constraint}`);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Read the length of a flexible window, refusing one outside 1 to 1440.
|
|
16
|
+
*/
|
|
17
|
+
function windowLength(minutes) {
|
|
18
|
+
if (minutes === undefined) {
|
|
19
|
+
throw new SimSchedulerValidationException("Invalid parameter: FlexibleTimeWindow Reason: a FLEXIBLE window " +
|
|
20
|
+
"needs MaximumWindowInMinutes, from 1 to 1440");
|
|
21
|
+
}
|
|
22
|
+
if (!Number.isSafeInteger(minutes)) {
|
|
23
|
+
throw new SimSchedulerValidationException(`Invalid parameter: FlexibleTimeWindow Reason: MaximumWindowInMinutes ` +
|
|
24
|
+
`is a whole number of minutes, and ${String(minutes)} is not`);
|
|
25
|
+
}
|
|
26
|
+
if (minutes < minimumWindowInMinutes) {
|
|
27
|
+
throw constraintFailure(String(minutes), "maximumWindowInMinutes", `have value greater than or equal to ${String(minimumWindowInMinutes)}`);
|
|
28
|
+
}
|
|
29
|
+
if (minutes > maximumWindowInMinutes) {
|
|
30
|
+
throw constraintFailure(String(minutes), "maximumWindowInMinutes", `have value less than or equal to ${String(maximumWindowInMinutes)}`);
|
|
31
|
+
}
|
|
32
|
+
return minutes;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* When, relative to its due time, a schedule invokes its target.
|
|
36
|
+
*
|
|
37
|
+
* With `OFF` that is the due time itself. With `FLEXIBLE` it is some moment
|
|
38
|
+
* after the due time and inside the window, which AWS chooses and does not
|
|
39
|
+
* reveal. Firing on the dot instead would let a test pass by relying on
|
|
40
|
+
* timing AWS does not promise, so each occurrence draws its own moment.
|
|
41
|
+
*/
|
|
42
|
+
export class SimSchedulerTimeWindow {
|
|
43
|
+
/**
|
|
44
|
+
* The window length in minutes, or nothing when the mode is `OFF`.
|
|
45
|
+
*/
|
|
46
|
+
minutes;
|
|
47
|
+
constructor(minutes) {
|
|
48
|
+
this.minutes = minutes;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Read the window a request carries, which AWS requires on every one.
|
|
52
|
+
*/
|
|
53
|
+
static of(window) {
|
|
54
|
+
const mode = window?.Mode;
|
|
55
|
+
if (mode === undefined) {
|
|
56
|
+
throw new SimSchedulerValidationException("FlexibleTimeWindow is required, and its Mode is OFF or FLEXIBLE");
|
|
57
|
+
}
|
|
58
|
+
if (mode === "FLEXIBLE") {
|
|
59
|
+
return new SimSchedulerTimeWindow(windowLength(window?.MaximumWindowInMinutes));
|
|
60
|
+
}
|
|
61
|
+
if (mode !== "OFF") {
|
|
62
|
+
throw constraintFailure(mode, "mode", "satisfy enum value set: [OFF, FLEXIBLE]");
|
|
63
|
+
}
|
|
64
|
+
if (window?.MaximumWindowInMinutes !== undefined) {
|
|
65
|
+
throw new SimSchedulerValidationException("Invalid parameter: FlexibleTimeWindow Reason: " +
|
|
66
|
+
"MaximumWindowInMinutes is only allowed when Mode is FLEXIBLE");
|
|
67
|
+
}
|
|
68
|
+
return new SimSchedulerTimeWindow(undefined);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The window as GetSchedule reports it, which is what the request sent.
|
|
72
|
+
*/
|
|
73
|
+
get declared() {
|
|
74
|
+
if (this.minutes === undefined) {
|
|
75
|
+
return { Mode: "OFF" };
|
|
76
|
+
}
|
|
77
|
+
return { Mode: "FLEXIBLE", MaximumWindowInMinutes: this.minutes };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The instant one occurrence invokes its target, drawn from `random`.
|
|
81
|
+
*
|
|
82
|
+
* It is never before the due time and always before the window closes, so
|
|
83
|
+
* a test that moves the clock past the end of the window sees the
|
|
84
|
+
* invocation whatever was drawn.
|
|
85
|
+
*/
|
|
86
|
+
invocationAt(due, random) {
|
|
87
|
+
if (this.minutes === undefined) {
|
|
88
|
+
return due;
|
|
89
|
+
}
|
|
90
|
+
const offset = Math.floor(random.next() * this.minutes * millisecondsPerMinute);
|
|
91
|
+
return new Date(due.getTime() + offset);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type BackgroundScheduler } from "../../util/background/background.js";
|
|
2
|
+
import { type SimRandom } from "../../util/random/sim-random.js";
|
|
2
3
|
import type { SimSdkCommandRouter } from "../../sdk/router/sim-sdk-command-router.type.js";
|
|
3
4
|
import type { SimAwsAccountRegionScope } from "../aws/sim-aws-account-region-scope.js";
|
|
4
5
|
import type { SimIamInterServiceAuthZ } from "../iam/authorize/sim-iam-inter-service-auth-z.js";
|
|
@@ -21,6 +22,11 @@ interface SimSchedulerProperties {
|
|
|
21
22
|
* in another simulated service is only reachable through SimAws.
|
|
22
23
|
*/
|
|
23
24
|
readonly deliveryTargets?: SimSchedulerDeliveryTargets;
|
|
25
|
+
/**
|
|
26
|
+
* Where a flexible time window draws the moment each occurrence is invoked,
|
|
27
|
+
* which is the host's `Math.random` unless a test supplies its own.
|
|
28
|
+
*/
|
|
29
|
+
readonly random?: SimRandom;
|
|
24
30
|
}
|
|
25
31
|
/**
|
|
26
32
|
* Simulated EventBridge Scheduler. Handles SDK commands. Emulates AWS
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BackgroundTasks, } from "../../util/background/background.js";
|
|
2
|
+
import { SimHostRandom } from "../../util/random/sim-random.js";
|
|
2
3
|
import { simAwsAccountRegionScopeFactory } from "../aws/sim-aws-account-region-scope.factory.js";
|
|
3
4
|
import { simIamInRegion } from "../iam/authorize/sim-iam-region-auth-z.js";
|
|
4
5
|
import { SimSchedulerCommands } from "./command/sim-scheduler-commands.js";
|
|
@@ -30,7 +31,7 @@ export class SimScheduler extends SimSchedulerInspection {
|
|
|
30
31
|
});
|
|
31
32
|
constructor(properties = {}) {
|
|
32
33
|
super();
|
|
33
|
-
const { accountRegionScope = simAwsAccountRegionScopeFactory.make(), background = new BackgroundTasks(), } = properties;
|
|
34
|
+
const { accountRegionScope = simAwsAccountRegionScopeFactory.make(), background = new BackgroundTasks(), random = new SimHostRandom(), } = properties;
|
|
34
35
|
const iam = simIamInRegion(properties.iam, accountRegionScope.regionName);
|
|
35
36
|
this.background = background;
|
|
36
37
|
this.groupStore = new SimSchedulerScheduleGroupStore({
|
|
@@ -44,6 +45,7 @@ export class SimScheduler extends SimSchedulerInspection {
|
|
|
44
45
|
background,
|
|
45
46
|
deliveryTargets: properties.deliveryTargets,
|
|
46
47
|
accountRegionScope,
|
|
48
|
+
random,
|
|
47
49
|
});
|
|
48
50
|
}
|
|
49
51
|
/**
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a simulation draws the values AWS leaves to chance.
|
|
3
|
+
*
|
|
4
|
+
* Some AWS behaviour is unpredictable by design, such as the moment inside a
|
|
5
|
+
* Scheduler flexible time window at which the target is invoked. A simulation
|
|
6
|
+
* draws those values from here, so a test can make them repeatable by passing
|
|
7
|
+
* a seeded source and leave them unpredictable otherwise.
|
|
8
|
+
*/
|
|
9
|
+
export interface SimRandom {
|
|
10
|
+
/**
|
|
11
|
+
* The next value, at least 0 and less than 1.
|
|
12
|
+
*/
|
|
13
|
+
next(): number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Draws from the host's `Math.random`, so values differ from run to run.
|
|
17
|
+
*
|
|
18
|
+
* This is the default, because a test that passes only for one particular
|
|
19
|
+
* draw is relying on something AWS does not promise.
|
|
20
|
+
*/
|
|
21
|
+
export declare class SimHostRandom implements SimRandom {
|
|
22
|
+
next(): number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Draws a repeatable sequence from a seed.
|
|
26
|
+
*
|
|
27
|
+
* Two sources built from the same seed give the same values in the same order,
|
|
28
|
+
* so a failure seen once can be reproduced. The generator is mulberry32, which
|
|
29
|
+
* is small and fast and has nothing to do with security.
|
|
30
|
+
*/
|
|
31
|
+
export declare class SimSeededRandom implements SimRandom {
|
|
32
|
+
private state;
|
|
33
|
+
constructor(seed: number);
|
|
34
|
+
next(): number;
|
|
35
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Draws from the host's `Math.random`, so values differ from run to run.
|
|
3
|
+
*
|
|
4
|
+
* This is the default, because a test that passes only for one particular
|
|
5
|
+
* draw is relying on something AWS does not promise.
|
|
6
|
+
*/
|
|
7
|
+
export class SimHostRandom {
|
|
8
|
+
next() {
|
|
9
|
+
return Math.random();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Draws a repeatable sequence from a seed.
|
|
14
|
+
*
|
|
15
|
+
* Two sources built from the same seed give the same values in the same order,
|
|
16
|
+
* so a failure seen once can be reproduced. The generator is mulberry32, which
|
|
17
|
+
* is small and fast and has nothing to do with security.
|
|
18
|
+
*/
|
|
19
|
+
export class SimSeededRandom {
|
|
20
|
+
state;
|
|
21
|
+
constructor(seed) {
|
|
22
|
+
// Only the low 32 bits of the seed take part, as the generator works on
|
|
23
|
+
// 32-bit integers.
|
|
24
|
+
this.state = Math.trunc(seed) >>> 0;
|
|
25
|
+
}
|
|
26
|
+
next() {
|
|
27
|
+
this.state = (this.state + 0x6d_2b_79_f5) >>> 0;
|
|
28
|
+
let mixed = this.state;
|
|
29
|
+
mixed = Math.imul(mixed ^ (mixed >>> 15), mixed | 1);
|
|
30
|
+
mixed ^= mixed + Math.imul(mixed ^ (mixed >>> 7), mixed | 61);
|
|
31
|
+
return ((mixed ^ (mixed >>> 14)) >>> 0) / 4_294_967_296;
|
|
32
|
+
}
|
|
33
|
+
}
|