@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.
Files changed (49) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +1 -0
  3. package/dist/sdk/error/sim-sdk.error.d.ts +18 -0
  4. package/dist/sdk/error/sim-sdk.error.js +41 -0
  5. package/dist/sdk/index.d.ts +1 -1
  6. package/dist/sdk/index.js +1 -1
  7. package/dist/sdk/send-abort.d.ts +28 -0
  8. package/dist/sdk/send-abort.js +112 -0
  9. package/dist/sdk/send-patch.js +3 -1
  10. package/dist/service/aws/factory/sim-aws-account-region-service-builder.js +1 -0
  11. package/dist/service/aws/sim-aws-properties.d.ts +8 -0
  12. package/dist/service/aws/sim-aws.d.ts +5 -0
  13. package/dist/service/aws/sim-aws.js +7 -1
  14. package/dist/service/dynamodb/command/authorize/sim-dynamodb-authorizer.d.ts +19 -1
  15. package/dist/service/dynamodb/command/authorize/sim-dynamodb-authorizer.js +21 -3
  16. package/dist/service/dynamodb/command/authorize/sim-dynamodb-reached.d.ts +6 -3
  17. package/dist/service/dynamodb/command/query/sim-dynamodb-query.js +3 -1
  18. package/dist/service/dynamodb/command/scan/sim-dynamodb-scan.js +3 -1
  19. package/dist/service/dynamodb/command/table/sim-dynamodb-table-reach.d.ts +4 -0
  20. package/dist/service/dynamodb/command/table/sim-dynamodb-table-reach.js +1 -0
  21. package/dist/service/dynamodb/table/sim-dynamodb-table-arn.d.ts +13 -0
  22. package/dist/service/dynamodb/table/sim-dynamodb-table-arn.js +20 -0
  23. package/dist/service/scheduler/cfn/sim-cfn-schedule-properties.d.ts +0 -4
  24. package/dist/service/scheduler/cfn/sim-cfn-schedule-properties.js +2 -19
  25. package/dist/service/scheduler/cfn/sim-cfn-schedule-time-window.d.ts +10 -0
  26. package/dist/service/scheduler/cfn/sim-cfn-schedule-time-window.js +37 -0
  27. package/dist/service/scheduler/command/schedule/sim-scheduler-described-schedule.d.ts +1 -2
  28. package/dist/service/scheduler/command/schedule/sim-scheduler-described-schedule.js +2 -3
  29. package/dist/service/scheduler/command/schedule/sim-scheduler-schedule-writer.js +3 -0
  30. package/dist/service/scheduler/command/schedule/sim-scheduler-unsimulated-input.js +0 -25
  31. package/dist/service/scheduler/command/sim-scheduler-commands.d.ts +2 -0
  32. package/dist/service/scheduler/command/sim-scheduler-commands.js +1 -0
  33. package/dist/service/scheduler/schedule/sim-scheduler-pending-occurrences.d.ts +23 -0
  34. package/dist/service/scheduler/schedule/sim-scheduler-pending-occurrences.js +36 -0
  35. package/dist/service/scheduler/schedule/sim-scheduler-schedule.d.ts +7 -0
  36. package/dist/service/scheduler/schedule/sim-scheduler-schedule.js +5 -0
  37. package/dist/service/scheduler/schedule/sim-scheduler-schedules.d.ts +27 -1
  38. package/dist/service/scheduler/schedule/sim-scheduler-schedules.js +52 -8
  39. package/dist/service/scheduler/schedule/sim-scheduler-time-window.d.ts +33 -0
  40. package/dist/service/scheduler/schedule/sim-scheduler-time-window.js +93 -0
  41. package/dist/service/scheduler/sim-scheduler.d.ts +6 -0
  42. package/dist/service/scheduler/sim-scheduler.js +3 -1
  43. package/dist/util/random/sim-random.d.ts +35 -0
  44. package/dist/util/random/sim-random.js +33 -0
  45. package/docs/sdk/README.md +23 -0
  46. package/docs/services/dynamodb/README.md +26 -0
  47. package/docs/services/scheduler/README.md +68 -5
  48. package/docs/time/README.md +18 -0
  49. 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. `FlexibleTimeWindow` always reports `OFF`
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. `FlexibleTimeWindow` always reports `OFF`
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: { Mode: "OFF" },
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.
@@ -43,6 +43,7 @@ export class SimSchedulerCommands {
43
43
  schedules,
44
44
  delivery: this.delivery,
45
45
  background,
46
+ random: properties.random,
46
47
  });
47
48
  this.scheduleCreation = new SimSchedulerCreateSchedule({
48
49
  schedules,
@@ -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
- * Fire a schedule that has fallen due, and arm it for the next time.
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.fire(schedule, due);
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
- * Fire a schedule that has fallen due, and arm it for the next time.
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
- if (this.schedules.find(schedule.groupName, schedule.name.value) !== schedule) {
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 (schedule.schedule.nextAfter(due) !== undefined) {
62
- this.armAfter(schedule, due);
63
- return;
101
+ if (completed) {
102
+ this.completed(schedule, invoked);
64
103
  }
65
- this.completed(schedule, invoked);
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
+ }