@kensio/yulin 1.21.20 → 1.21.22

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 (88) hide show
  1. package/dist/service/athena/engine/sim-athena-engine-result.d.ts +9 -1
  2. package/dist/service/athena/engine/sim-athena-engine-result.js +15 -4
  3. package/dist/service/athena/engine/sim-athena-engine-run.d.ts +2 -0
  4. package/dist/service/athena/engine/sim-athena-engine-run.js +6 -1
  5. package/dist/service/athena/engine/sim-athena-query-engine.js +1 -0
  6. package/dist/service/athena/engine/sim-athena-result-columns.d.ts +1 -1
  7. package/dist/service/athena/engine/sim-athena-result-columns.js +11 -4
  8. package/dist/service/athena/engine/sim-athena-round-scales.d.ts +19 -0
  9. package/dist/service/athena/engine/sim-athena-round-scales.js +80 -0
  10. package/dist/service/athena/engine/sim-athena-sql-translation.d.ts +9 -0
  11. package/dist/service/athena/engine/sim-athena-sql-translation.js +17 -2
  12. package/dist/service/lambda/cfn/event-source-mapping/sim-cfn-lambda-event-source-mapping-properties.js +2 -0
  13. package/dist/service/lambda/cfn/event-source-mapping/sim-cfn-lambda-event-source-mapping-property-names.js +1 -5
  14. package/dist/service/lambda/command/event-source-mapping/create-event-source-mapping-input.d.ts +5 -0
  15. package/dist/service/lambda/command/event-source-mapping/create-event-source-mapping-input.js +7 -0
  16. package/dist/service/lambda/command/event-source-mapping/create-event-source-mapping-refusals.js +0 -1
  17. package/dist/service/lambda/command/event-source-mapping/create-event-source-mapping.handler.js +1 -0
  18. package/dist/service/lambda/command/event-source-mapping/event-source-mapping.command.d.ts +2 -1
  19. package/dist/service/lambda/command/event-source-mapping/event-source-mapping.command.js +0 -6
  20. package/dist/service/lambda/event-source/filter/sim-lambda-filter-criteria.d.ts +48 -0
  21. package/dist/service/lambda/event-source/filter/sim-lambda-filter-criteria.js +129 -0
  22. package/dist/service/lambda/event-source/filter/sim-lambda-filtered-delivery.d.ts +27 -0
  23. package/dist/service/lambda/event-source/filter/sim-lambda-filtered-delivery.js +15 -0
  24. package/dist/service/lambda/event-source/filter/sim-lambda-filtered-records.d.ts +24 -0
  25. package/dist/service/lambda/event-source/filter/sim-lambda-filtered-records.js +35 -0
  26. package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-shard-poller.d.ts +7 -4
  27. package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-shard-poller.js +9 -6
  28. package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-stream-delivery.d.ts +16 -4
  29. package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-stream-delivery.js +36 -5
  30. package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-stream-event-source-poller.d.ts +4 -3
  31. package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-stream-event-source-poller.js +6 -7
  32. package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-stream-event.d.ts +14 -2
  33. package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-stream-event.js +21 -2
  34. package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-delivery.d.ts +19 -6
  35. package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-delivery.js +41 -7
  36. package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-event-source-poller.d.ts +7 -3
  37. package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-event-source-poller.js +10 -9
  38. package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-event.d.ts +6 -2
  39. package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-event.js +6 -2
  40. package/dist/service/lambda/event-source/poll/sim-lambda-event-source-delivery.d.ts +21 -1
  41. package/dist/service/lambda/event-source/poll/sim-lambda-event-source-delivery.js +32 -2
  42. package/dist/service/lambda/event-source/poll/sim-lambda-sqs-delivery.js +1 -0
  43. package/dist/service/lambda/event-source/poll/sim-lambda-sqs-event.d.ts +8 -0
  44. package/dist/service/lambda/event-source/poll/sim-lambda-sqs-event.js +11 -0
  45. package/dist/service/lambda/event-source/sim-lambda-event-source-mapping.d.ts +10 -0
  46. package/dist/service/lambda/event-source/sim-lambda-event-source-mapping.js +11 -0
  47. package/dist/service/lambda/event-source/stream/sim-lambda-stream-cascade-guard.d.ts +35 -8
  48. package/dist/service/lambda/event-source/stream/sim-lambda-stream-cascade-guard.js +35 -12
  49. package/dist/service/lambda/event-source/stream/sim-lambda-stream-cascade.error.d.ts +13 -4
  50. package/dist/service/lambda/event-source/stream/sim-lambda-stream-cascade.error.js +12 -6
  51. package/dist/service/lambda/event-source/stream/sim-lambda-stream-halt.d.ts +18 -0
  52. package/dist/service/lambda/event-source/stream/sim-lambda-stream-halt.js +22 -0
  53. package/dist/service/lambda/function/invoke/sim-lambda-invocation-deadline.d.ts +4 -0
  54. package/dist/service/lambda/function/invoke/sim-lambda-invocation-deadline.js +21 -0
  55. package/dist/service/lambda/function/invoke/sim-lambda-stall-watch.d.ts +38 -0
  56. package/dist/service/lambda/function/invoke/sim-lambda-stall-watch.js +78 -0
  57. package/dist/service/lambda/function/invoke/sim-lambda-stalled.error.d.ts +23 -0
  58. package/dist/service/lambda/function/invoke/sim-lambda-stalled.error.js +22 -0
  59. package/dist/service/lambda/function/invoke/timer/sim-lambda-clock-timer.d.ts +4 -0
  60. package/dist/service/lambda/function/invoke/timer/sim-lambda-clock-timer.js +8 -0
  61. package/dist/service/lambda/function/invoke/timer/sim-lambda-invocation-timers.d.ts +10 -0
  62. package/dist/service/lambda/function/invoke/timer/sim-lambda-invocation-timers.js +28 -0
  63. package/dist/service/lambda/function/invoke/timer/sim-lambda-pending-timer.d.ts +9 -0
  64. package/dist/service/lambda/function/invoke/timer/sim-lambda-pending-timer.js +1 -0
  65. package/dist/service/scheduler/command/schedule/schedule.command.d.ts +1 -0
  66. package/dist/service/scheduler/command/schedule/sim-scheduler-described-schedule.js +1 -0
  67. package/dist/service/scheduler/command/schedule/sim-scheduler-schedule-writer.js +2 -1
  68. package/dist/service/scheduler/command/schedule/sim-scheduler-unsimulated-input.d.ts +3 -3
  69. package/dist/service/scheduler/command/schedule/sim-scheduler-unsimulated-input.js +19 -11
  70. package/dist/service/scheduler/schedule/sim-scheduler-schedule-expression.d.ts +5 -3
  71. package/dist/service/scheduler/schedule/sim-scheduler-schedule-expression.js +6 -4
  72. package/dist/service/scheduler/schedule/sim-scheduler-schedule.d.ts +9 -0
  73. package/dist/service/scheduler/schedule/sim-scheduler-schedule.js +8 -0
  74. package/dist/util/schedule/at/sim-at-expression.d.ts +2 -1
  75. package/dist/util/schedule/at/sim-at-expression.js +7 -3
  76. package/dist/util/schedule/cron/sim-cron-expression.d.ts +6 -3
  77. package/dist/util/schedule/cron/sim-cron-expression.js +24 -21
  78. package/dist/util/schedule/sim-schedule-zone-clock.d.ts +26 -0
  79. package/dist/util/schedule/sim-schedule-zone-clock.js +56 -0
  80. package/dist/util/schedule/sim-schedule-zone.d.ts +58 -0
  81. package/dist/util/schedule/sim-schedule-zone.js +83 -0
  82. package/dist/util/schedule/sim-schedule.d.ts +1 -1
  83. package/dist/util/schedule/sim-schedule.js +6 -5
  84. package/docs/services/athena/README.md +5 -0
  85. package/docs/services/lambda/README.md +159 -15
  86. package/docs/services/scheduler/README.md +35 -6
  87. package/docs/time/README.md +6 -0
  88. package/package.json +1 -1
@@ -0,0 +1,78 @@
1
+ import { clearTimeout, setTimeout } from "node:timers";
2
+ const millisecondsPerSecond = 1000;
3
+ /** The least host time a stopped clock gets before an invocation is stalled. */
4
+ const shortestWatchMilliseconds = 1000;
5
+ /** The most host time a stopped clock gets, whatever the function's timeout. */
6
+ const longestWatchMilliseconds = 2000;
7
+ /**
8
+ * A watch on host time for an invocation the simulated clock has left behind.
9
+ *
10
+ * A handler's timers wait on the simulation's clock, and so does the
11
+ * invocation's deadline. Simulated time standing still holds both, and the
12
+ * invocation runs on until the test framework gives up on the test around it.
13
+ * Elapsed host time is the only thing separating that from a test meaning to
14
+ * start an invocation, advance the clock and then wait for it, which is why
15
+ * host time is what this watches.
16
+ *
17
+ * A turn that finds simulated time has moved starts the wait again, so a test
18
+ * advancing the clock in steps is left alone. The host timer is unreferenced,
19
+ * and an invocation nobody is driving never holds the process open.
20
+ */
21
+ export class SimLambdaStallWatch {
22
+ #properties;
23
+ #delay;
24
+ #seenAt = 0;
25
+ #hostTimer;
26
+ constructor(properties) {
27
+ this.#properties = properties;
28
+ this.#delay = simLambdaStallWatchDelay(properties.timeoutSeconds);
29
+ }
30
+ /**
31
+ * Start watching, from the instant the clock reads now.
32
+ */
33
+ start() {
34
+ this.#seenAt = this.#properties.background.now().getTime();
35
+ this.#arm();
36
+ }
37
+ /**
38
+ * Give up watching, as an invocation that has ended does.
39
+ */
40
+ cancel() {
41
+ clearTimeout(this.#hostTimer);
42
+ this.#hostTimer = undefined;
43
+ }
44
+ #arm() {
45
+ this.#hostTimer = setTimeout(() => {
46
+ this.#hostTimer = undefined;
47
+ this.#check();
48
+ }, this.#delay);
49
+ this.#hostTimer.unref();
50
+ }
51
+ /**
52
+ * See whether the invocation still has a way to finish, and watch on where
53
+ * it has: either simulated time has moved since the last turn, or the
54
+ * handler is busy with something other than a timer.
55
+ */
56
+ #check() {
57
+ const now = this.#properties.background.now().getTime();
58
+ const waitingOn = this.#properties.waitingOn();
59
+ if (waitingOn === undefined || now !== this.#seenAt) {
60
+ this.#seenAt = now;
61
+ this.#arm();
62
+ return;
63
+ }
64
+ this.#properties.stalled(waitingOn, this.#delay);
65
+ }
66
+ }
67
+ /**
68
+ * How long the host waits before it calls an invocation stalled.
69
+ *
70
+ * The function's own timeout is the bound that fits, since an invocation the
71
+ * deadline would have ended already has nothing left to wait for. It is held
72
+ * between one and two seconds. Below that a loaded host could fail a test that
73
+ * was about to advance the clock, and above it a test framework's own timeout
74
+ * arrives first and takes the diagnostic with it.
75
+ */
76
+ function simLambdaStallWatchDelay(timeoutSeconds) {
77
+ return Math.min(Math.max(timeoutSeconds * millisecondsPerSecond, shortestWatchMilliseconds), longestWatchMilliseconds);
78
+ }
@@ -0,0 +1,23 @@
1
+ import { SimLambdaRuntimeError } from "../../error/sim-lambda-runtime.error.js";
2
+ import type { SimLambdaPendingTimer } from "./timer/sim-lambda-pending-timer.js";
3
+ interface SimLambdaStalledProperties {
4
+ readonly awsRequestId: string;
5
+ /** The simulated instant the clock has stopped at. */
6
+ readonly at: Date;
7
+ /** The timer the invocation is waiting on. */
8
+ readonly waitingOn: SimLambdaPendingTimer;
9
+ /** How long the host has watched simulated time stand still. */
10
+ readonly waitedMilliseconds: number;
11
+ }
12
+ /**
13
+ * The error an invocation gets when nothing is left that could end it.
14
+ *
15
+ * A handler's timers wait on the simulation's clock, and so does the
16
+ * invocation's own deadline. Simulated time standing still holds both, and the
17
+ * invocation would otherwise run until the test framework gave up on the test
18
+ * around it, reporting the test rather than the timer. This names the timer
19
+ * the handler is waiting on, the instant it is due at, and what to do about
20
+ * it.
21
+ */
22
+ export declare function simLambdaStalledError(properties: SimLambdaStalledProperties): SimLambdaRuntimeError;
23
+ export {};
@@ -0,0 +1,22 @@
1
+ import { SimLambdaRuntimeError } from "../../error/sim-lambda-runtime.error.js";
2
+ /** What Yulin calls an invocation the clock is never going to release. */
3
+ const stalledErrorType = "Yulin.StalledInvocation";
4
+ /**
5
+ * The error an invocation gets when nothing is left that could end it.
6
+ *
7
+ * A handler's timers wait on the simulation's clock, and so does the
8
+ * invocation's own deadline. Simulated time standing still holds both, and the
9
+ * invocation would otherwise run until the test framework gave up on the test
10
+ * around it, reporting the test rather than the timer. This names the timer
11
+ * the handler is waiting on, the instant it is due at, and what to do about
12
+ * it.
13
+ */
14
+ export function simLambdaStalledError(properties) {
15
+ const { awsRequestId, at, waitingOn, waitedMilliseconds } = properties;
16
+ const error = new SimLambdaRuntimeError(stalledErrorType, `${at.toISOString()} ${awsRequestId} Task is waiting on a ${waitingOn.delay} ms timer due at ${waitingOn.dueTime.toISOString()}. Simulated time has stood still for ${waitedMilliseconds} ms of host time, leaving both that timer and the invocation deadline out of reach. Advance the simulation's clock past the timer delay to release it.`);
17
+ // Nothing in the handler threw, so its frames say nothing about why the
18
+ // invocation is stuck, and the simulator's own frames say less. The timeout
19
+ // this stands in for is reported without a stack too.
20
+ error.stack = `${error.name}: ${error.message}`;
21
+ return error;
22
+ }
@@ -21,6 +21,10 @@ interface SimLambdaClockTimerProperties {
21
21
  export declare class SimLambdaClockTimer {
22
22
  #private;
23
23
  constructor(properties: SimLambdaClockTimerProperties);
24
+ /** The delay this timer was last started with, in milliseconds. */
25
+ get delay(): number;
26
+ /** The simulated instant this timer's work is due to run at. */
27
+ get dueTime(): Date;
24
28
  /**
25
29
  * Wait a delay of simulated time, and answer with how long that turned out
26
30
  * to be. A delay of nothing leaves the timer due where the clock already
@@ -31,6 +31,14 @@ export class SimLambdaClockTimer {
31
31
  this.#run = properties.run;
32
32
  this.#dueTime = properties.background.now();
33
33
  }
34
+ /** The delay this timer was last started with, in milliseconds. */
35
+ get delay() {
36
+ return this.#delay;
37
+ }
38
+ /** The simulated instant this timer's work is due to run at. */
39
+ get dueTime() {
40
+ return new Date(this.#dueTime);
41
+ }
34
42
  /**
35
43
  * Wait a delay of simulated time, and answer with how long that turned out
36
44
  * to be. A delay of nothing leaves the timer due where the clock already
@@ -1,4 +1,5 @@
1
1
  import type { BackgroundScheduler } from "../../../../../util/background/background.js";
2
+ import type { SimLambdaPendingTimer } from "./sim-lambda-pending-timer.js";
2
3
  import { type SimLambdaTimerCallback, SimLambdaTimerHandle, type SimLambdaTimerOwner, type SimLambdaTimers } from "./sim-lambda-timer-handle.js";
3
4
  interface SimLambdaInvocationTimersProperties {
4
5
  readonly background: BackgroundScheduler;
@@ -31,6 +32,15 @@ export declare class SimLambdaInvocationTimers implements SimLambdaTimers, SimLa
31
32
  * Run a callback every time simulated time moves on by a delay.
32
33
  */
33
34
  setInterval(callback: SimLambdaTimerCallback, delay?: number, ...arguments_: unknown[]): SimLambdaTimerHandle;
35
+ /**
36
+ * The timer this invocation is waiting on, the earliest due one where it
37
+ * has several.
38
+ *
39
+ * A timer due at the instant the clock already reads is waiting for nothing:
40
+ * the next turn of the host event loop runs it. Only a timer the clock has
41
+ * yet to reach counts.
42
+ */
43
+ pending(): SimLambdaPendingTimer | undefined;
34
44
  /**
35
45
  * Give up one timer, as `clearTimeout` and `clearInterval` do.
36
46
  */
@@ -42,6 +42,27 @@ export class SimLambdaInvocationTimers {
42
42
  this.#call(callback, arguments_);
43
43
  });
44
44
  }
45
+ /**
46
+ * The timer this invocation is waiting on, the earliest due one where it
47
+ * has several.
48
+ *
49
+ * A timer due at the instant the clock already reads is waiting for nothing:
50
+ * the next turn of the host event loop runs it. Only a timer the clock has
51
+ * yet to reach counts.
52
+ */
53
+ pending() {
54
+ const now = this.#background.now().getTime();
55
+ let earliest;
56
+ for (const timer of this.#running.values()) {
57
+ if (timer.dueTime.getTime() > now && isEarlier(timer, earliest)) {
58
+ earliest = timer;
59
+ }
60
+ }
61
+ if (earliest === undefined) {
62
+ return undefined;
63
+ }
64
+ return { delay: earliest.delay, dueTime: earliest.dueTime };
65
+ }
45
66
  /**
46
67
  * Give up one timer, as `clearTimeout` and `clearInterval` do.
47
68
  */
@@ -110,3 +131,10 @@ export class SimLambdaInvocationTimers {
110
131
  this.#released = undefined;
111
132
  }
112
133
  }
134
+ /**
135
+ * Whether a timer comes due before the earliest one found so far. The first
136
+ * timer looked at has nothing to beat.
137
+ */
138
+ function isEarlier(timer, earliest) {
139
+ return earliest === undefined || timer.dueTime < earliest.dueTime;
140
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * A timer one invocation is waiting on, and the instant it comes due.
3
+ */
4
+ export interface SimLambdaPendingTimer {
5
+ /** The delay the code under test asked for, in milliseconds. */
6
+ readonly delay: number;
7
+ /** The simulated instant the timer's work runs at. */
8
+ readonly dueTime: Date;
9
+ }
@@ -95,6 +95,7 @@ export interface SimGetScheduleCommandOutput {
95
95
  readonly Name?: string | undefined;
96
96
  readonly GroupName?: string | undefined;
97
97
  readonly ScheduleExpression?: string | undefined;
98
+ readonly ScheduleExpressionTimezone?: string | undefined;
98
99
  readonly State?: string | undefined;
99
100
  readonly Description?: string | undefined;
100
101
  readonly ActionAfterCompletion?: string | undefined;
@@ -12,6 +12,7 @@ export function describedSchedule(schedule) {
12
12
  Name: schedule.name.value,
13
13
  GroupName: schedule.groupName,
14
14
  ScheduleExpression: schedule.schedule.source,
15
+ ScheduleExpressionTimezone: schedule.timeZone,
15
16
  State: schedule.state.value,
16
17
  Description: schedule.description,
17
18
  ActionAfterCompletion: schedule.actionAfterCompletion,
@@ -33,7 +33,8 @@ export class SimSchedulerScheduleWriter {
33
33
  name: requested.name,
34
34
  groupName: requested.groupName,
35
35
  accountRegionScope: this.accountRegionScope,
36
- schedule: schedulerSchedule(input.ScheduleExpression),
36
+ schedule: schedulerSchedule(input.ScheduleExpression, input.ScheduleExpressionTimezone),
37
+ timeZone: input.ScheduleExpressionTimezone,
37
38
  target: SimSchedulerTarget.of(input.Target),
38
39
  state: SimSchedulerScheduleState.of(input.State),
39
40
  actionAfterCompletion: actionAfterCompletionIn(input.ActionAfterCompletion),
@@ -2,8 +2,8 @@ import type { SimSchedulerScheduleInput } from "./schedule.command.js";
2
2
  /**
3
3
  * Refuse the schedule request inputs this simulation does not model.
4
4
  *
5
- * A timezone is refused rather than ignored: a schedule whose cron expression
6
- * quietly ran in UTC when it was written for another zone would fire at the
7
- * wrong hour, which is precisely the thing a test of a nightly job is checking.
5
+ * A timezone is one it does model. A name no zone answers to is refused here
6
+ * rather than left to the expression reader, which would report it against
7
+ * `ScheduleExpression` and send the caller to the wrong parameter.
8
8
  */
9
9
  export declare function refuseUnsimulatedScheduleInput(input: SimSchedulerScheduleInput): void;
@@ -1,10 +1,23 @@
1
1
  import { SimSchedulerUnsimulatedInputException, SimSchedulerValidationException, } from "../../error/sim-scheduler.error.js";
2
+ import { SimScheduleZone } from "../../../../util/schedule/sim-schedule-zone.js";
2
3
  import { refuseUnsimulatedTarget } from "./sim-scheduler-unsimulated-target.js";
3
4
  const maximumDescriptionLength = 512;
4
5
  /**
5
- * The timezone every simulated schedule runs in.
6
+ * Refuse a timezone no zone answers to, naming the parameter it came in on.
6
7
  */
7
- const simulatedTimezone = "UTC";
8
+ function refuseUnknownTimezone(timezone) {
9
+ if (timezone === undefined) {
10
+ return;
11
+ }
12
+ try {
13
+ SimScheduleZone.of(timezone);
14
+ }
15
+ catch {
16
+ throw new SimSchedulerValidationException(`Invalid parameter: ScheduleExpressionTimezone Reason: ` +
17
+ `'${timezone}' is not a timezone. A timezone is an IANA name such ` +
18
+ `as 'Europe/London', or 'UTC'`);
19
+ }
20
+ }
8
21
  /**
9
22
  * Read the flexible time window, which AWS requires on every request.
10
23
  *
@@ -32,19 +45,14 @@ function refuseFlexibleWindow(input) {
32
45
  /**
33
46
  * Refuse the schedule request inputs this simulation does not model.
34
47
  *
35
- * A timezone is refused rather than ignored: a schedule whose cron expression
36
- * quietly ran in UTC when it was written for another zone would fire at the
37
- * wrong hour, which is precisely the thing a test of a nightly job is checking.
48
+ * A timezone is one it does model. A name no zone answers to is refused here
49
+ * rather than left to the expression reader, which would report it against
50
+ * `ScheduleExpression` and send the caller to the wrong parameter.
38
51
  */
39
52
  export function refuseUnsimulatedScheduleInput(input) {
40
53
  refuseFlexibleWindow(input);
41
54
  refuseUnsimulatedTarget(input.Target);
42
- const timezone = input.ScheduleExpressionTimezone;
43
- if (timezone !== undefined && timezone !== simulatedTimezone) {
44
- throw new SimSchedulerUnsimulatedInputException(`ScheduleExpressionTimezone '${timezone}' is not simulated. Every ` +
45
- `simulated schedule runs in ${simulatedTimezone}, and running this ` +
46
- `one there anyway would fire it at the wrong hour.`);
47
- }
55
+ refuseUnknownTimezone(input.ScheduleExpressionTimezone);
48
56
  if (input.StartDate !== undefined || input.EndDate !== undefined) {
49
57
  throw new SimSchedulerUnsimulatedInputException("A schedule StartDate and EndDate are not simulated, so a schedule " +
50
58
  "carrying either is refused rather than created as one that fires " +
@@ -11,10 +11,12 @@ import type { SimScheduleDialect } from "../../../util/schedule/sim-schedule-dia
11
11
  */
12
12
  export declare const schedulerScheduleDialect: SimScheduleDialect;
13
13
  /**
14
- * Read the `ScheduleExpression` a request carried.
14
+ * Read the `ScheduleExpression` a request carried, in the zone it named.
15
15
  *
16
16
  * The shared parser knows nothing about Scheduler, so its refusals become this
17
17
  * service's own errors here. Which of the two a caller gets tells them whether
18
- * the expression is wrong or whether it is right and unsimulated.
18
+ * the expression is wrong or whether it is right and unsimulated. A timezone no
19
+ * zone answers to is refused the same way, since the parser is what knows which
20
+ * names there are.
19
21
  */
20
- export declare function schedulerSchedule(source: string | undefined): SimSchedule;
22
+ export declare function schedulerSchedule(source: string | undefined, timeZone?: string): SimSchedule;
@@ -21,13 +21,15 @@ export const schedulerScheduleDialect = {
21
21
  allowsOneTime: true,
22
22
  };
23
23
  /**
24
- * Read the `ScheduleExpression` a request carried.
24
+ * Read the `ScheduleExpression` a request carried, in the zone it named.
25
25
  *
26
26
  * The shared parser knows nothing about Scheduler, so its refusals become this
27
27
  * service's own errors here. Which of the two a caller gets tells them whether
28
- * the expression is wrong or whether it is right and unsimulated.
28
+ * the expression is wrong or whether it is right and unsimulated. A timezone no
29
+ * zone answers to is refused the same way, since the parser is what knows which
30
+ * names there are.
29
31
  */
30
- export function schedulerSchedule(source) {
32
+ export function schedulerSchedule(source, timeZone) {
31
33
  if (source === undefined || source === "") {
32
34
  throw new SimSchedulerValidationException("ScheduleExpression is required");
33
35
  }
@@ -36,7 +38,7 @@ export function schedulerSchedule(source) {
36
38
  `characters, and this one is ${String(source.length)}`);
37
39
  }
38
40
  try {
39
- return SimSchedule.of(source, schedulerScheduleDialect);
41
+ return SimSchedule.of(source, schedulerScheduleDialect, timeZone);
40
42
  }
41
43
  catch (error) {
42
44
  if (error instanceof SimUnsimulatedScheduleExpressionError) {
@@ -21,6 +21,8 @@ interface SimSchedulerScheduleProperties {
21
21
  readonly state: SimSchedulerScheduleState;
22
22
  readonly actionAfterCompletion: SimSchedulerActionAfterCompletion;
23
23
  readonly description?: string | undefined;
24
+ /** The timezone the request named, which a describe reports back. */
25
+ readonly timeZone?: string | undefined;
24
26
  readonly createdAt: Date;
25
27
  }
26
28
  /**
@@ -51,6 +53,13 @@ export declare class SimSchedulerSchedule {
51
53
  readonly target: SimSchedulerTarget;
52
54
  readonly actionAfterCompletion: SimSchedulerActionAfterCompletion;
53
55
  readonly description: string | undefined;
56
+ /**
57
+ * The timezone the schedule was created with, if it named one.
58
+ *
59
+ * A schedule created without one runs in UTC, and reports no timezone rather
60
+ * than reporting the one it fell back to.
61
+ */
62
+ readonly timeZone: string | undefined;
54
63
  readonly creationDate: Date;
55
64
  /**
56
65
  * When this schedule falls due, which is read as simulated time advances.
@@ -28,6 +28,13 @@ export class SimSchedulerSchedule {
28
28
  target;
29
29
  actionAfterCompletion;
30
30
  description;
31
+ /**
32
+ * The timezone the schedule was created with, if it named one.
33
+ *
34
+ * A schedule created without one runs in UTC, and reports no timezone rather
35
+ * than reporting the one it fell back to.
36
+ */
37
+ timeZone;
31
38
  creationDate;
32
39
  /**
33
40
  * When this schedule falls due, which is read as simulated time advances.
@@ -45,6 +52,7 @@ export class SimSchedulerSchedule {
45
52
  this.target = properties.target;
46
53
  this.actionAfterCompletion = properties.actionAfterCompletion;
47
54
  this.description = properties.description;
55
+ this.timeZone = properties.timeZone;
48
56
  this.creationDate = properties.createdAt;
49
57
  this.modified = properties.createdAt;
50
58
  this.held = properties.state;
@@ -1,3 +1,4 @@
1
+ import type { SimScheduleZone } from "../sim-schedule-zone.js";
1
2
  /**
2
3
  * One `at()` expression: a single instant, and never another.
3
4
  *
@@ -12,7 +13,7 @@ export declare class SimAtExpression {
12
13
  /**
13
14
  * Read the instant inside an `at(...)`.
14
15
  */
15
- static of(source: string): SimAtExpression;
16
+ static of(source: string, zone: SimScheduleZone): SimAtExpression;
16
17
  /**
17
18
  * The instant this expression names, once, and nothing after that.
18
19
  *
@@ -7,6 +7,7 @@ import { SimScheduleExpressionError } from "../sim-schedule.error.js";
7
7
  * schedule rather than part of the expression, which is why one written here
8
8
  * would be ambiguous rather than helpful.
9
9
  */
10
+ const millisecondsPerSecond = 1000;
10
11
  const atInstant = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})T(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})$/u;
11
12
  /**
12
13
  * One `at()` expression: a single instant, and never another.
@@ -25,7 +26,7 @@ export class SimAtExpression {
25
26
  /**
26
27
  * Read the instant inside an `at(...)`.
27
28
  */
28
- static of(source) {
29
+ static of(source, zone) {
29
30
  const read = atInstant.exec(source.trim())?.groups;
30
31
  if (read === undefined) {
31
32
  throw new SimScheduleExpressionError(`an at expression is an instant written 'yyyy-mm-ddThh:mm:ss', with ` +
@@ -37,11 +38,14 @@ export class SimAtExpression {
37
38
  // JavaScript rolls an impossible date over rather than refusing it, so the
38
39
  // thirtieth of February quietly becomes the second of March and half past
39
40
  // twenty-five becomes tomorrow morning. Writing the instant back out is
40
- // what catches every one of those in one comparison.
41
+ // what catches every one of those in one comparison. The calendar is read
42
+ // in UTC for this, since which instant the wall-clock time is depends on
43
+ // the zone and whether it is a real time of day does not.
41
44
  if (!at.toISOString().startsWith(written)) {
42
45
  throw new SimScheduleExpressionError(`an at expression names a real instant, and '${source}' is not one`);
43
46
  }
44
- return new this(source, at);
47
+ const minuteStart = zone.instantOf(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute));
48
+ return new this(source, new Date(minuteStart + Number(second) * millisecondsPerSecond));
45
49
  }
46
50
  /**
47
51
  * The instant this expression names, once, and nothing after that.
@@ -1,13 +1,16 @@
1
+ import type { SimScheduleZone } from "../sim-schedule-zone.js";
1
2
  import type { SimCronFieldSpec } from "./sim-cron-field-spec.js";
2
3
  /**
3
4
  * One cron expression, read into the instants it falls due at.
4
5
  *
5
- * Everything is read in UTC. AWS runs scheduled rules in UTC and offers a
6
- * timezone as a separate setting, so a local timezone never enters into this.
6
+ * The fields name a wall-clock time, and the schedule's zone is what says which
7
+ * instants those are. An EventBridge rule has no timezone setting and gets the
8
+ * UTC zone, which reads exactly as the UTC calendar does.
7
9
  */
8
10
  export declare class SimCronExpression {
9
11
  readonly source: string;
10
12
  private readonly fields;
13
+ private readonly zone;
11
14
  private constructor();
12
15
  /**
13
16
  * Read a cron expression against a dialect's fields.
@@ -16,7 +19,7 @@ export declare class SimCronExpression {
16
19
  * is refused naming the form expected rather than read as though the missing
17
20
  * one were at the end.
18
21
  */
19
- static of(specs: readonly SimCronFieldSpec[], source: string): SimCronExpression;
22
+ static of(specs: readonly SimCronFieldSpec[], source: string, zone: SimScheduleZone): SimCronExpression;
20
23
  /**
21
24
  * The next instant this expression falls due after an instant.
22
25
  *
@@ -17,15 +17,18 @@ function startOfNextMinute(instant) {
17
17
  /**
18
18
  * One cron expression, read into the instants it falls due at.
19
19
  *
20
- * Everything is read in UTC. AWS runs scheduled rules in UTC and offers a
21
- * timezone as a separate setting, so a local timezone never enters into this.
20
+ * The fields name a wall-clock time, and the schedule's zone is what says which
21
+ * instants those are. An EventBridge rule has no timezone setting and gets the
22
+ * UTC zone, which reads exactly as the UTC calendar does.
22
23
  */
23
24
  export class SimCronExpression {
24
25
  source;
25
26
  fields;
26
- constructor(source, fields) {
27
+ zone;
28
+ constructor(source, fields, zone) {
27
29
  this.source = source;
28
30
  this.fields = fields;
31
+ this.zone = zone;
29
32
  }
30
33
  /**
31
34
  * Read a cron expression against a dialect's fields.
@@ -34,14 +37,14 @@ export class SimCronExpression {
34
37
  * is refused naming the form expected rather than read as though the missing
35
38
  * one were at the end.
36
39
  */
37
- static of(specs, source) {
40
+ static of(specs, source, zone) {
38
41
  const written = source.trim().split(whitespace);
39
42
  if (written.length !== specs.length) {
40
43
  throw new SimScheduleExpressionError(`a cron expression has ${String(specs.length)} fields separated by ` +
41
44
  `spaces, ${specs.map((spec) => spec.name).join(" ")}, and this one ` +
42
45
  `has ${String(written.length)}`);
43
46
  }
44
- return new this(source, readFields(specs, written));
47
+ return new this(source, readFields(specs, written), zone);
45
48
  }
46
49
  /**
47
50
  * The next instant this expression falls due after an instant.
@@ -50,7 +53,7 @@ export class SimCronExpression {
50
53
  * has, since a cron expression naming only years in the past falls due never.
51
54
  */
52
55
  nextAfter(instant) {
53
- const end = Date.UTC(this.fields.year.maximum + 1, 0, 1);
56
+ const end = this.zone.instantOf(this.fields.year.maximum + 1, 0, 1);
54
57
  let candidate = startOfNextMinute(instant);
55
58
  while (candidate < end) {
56
59
  const skipTo = this.skipUnmatched(candidate);
@@ -71,27 +74,27 @@ export class SimCronExpression {
71
74
  * steps rather than the million minutes they hold.
72
75
  */
73
76
  skipUnmatched(at) {
74
- const date = new Date(at);
75
- const year = date.getUTCFullYear();
77
+ const read = this.zone.partsAt(at);
78
+ const { year, month, day, hour } = read;
76
79
  if (!this.fields.year.allows(year)) {
77
- return Date.UTC(year + 1, 0, 1);
80
+ return this.zone.instantOf(year + 1, 0, 1);
78
81
  }
79
- const month = date.getUTCMonth();
80
82
  if (!this.fields.month.allows(month + 1)) {
81
- return Date.UTC(year, month + 1, 1);
83
+ return this.zone.instantOf(year, month + 1, 1);
82
84
  }
83
- const day = date.getUTCDate();
84
- if (!this.allowsDay(date)) {
85
- return Date.UTC(year, month, day + 1);
85
+ if (!this.allowsDay(read)) {
86
+ return this.zone.instantOf(year, month, day + 1);
86
87
  }
87
- const hour = date.getUTCHours();
88
88
  if (!this.fields.hours.allows(hour)) {
89
- return Date.UTC(year, month, day, hour + 1);
89
+ return this.zone.instantOf(year, month, day, hour + 1);
90
90
  }
91
- if (!this.fields.minutes.allows(date.getUTCMinutes())) {
91
+ if (!this.fields.minutes.allows(read.minute)) {
92
92
  return at + millisecondsPerMinute;
93
93
  }
94
- return undefined;
94
+ // The second of a repeated hour reads the same wall clock as the first and
95
+ // has already been answered with. Real Scheduler fires once when the clocks
96
+ // go back, rather than twice.
97
+ return this.zone.isCanonical(at) ? undefined : at + millisecondsPerMinute;
95
98
  }
96
99
  /**
97
100
  * Whether this expression names a day.
@@ -99,12 +102,12 @@ export class SimCronExpression {
99
102
  * Whichever of the two day fields is not `?` is the one that decides, which
100
103
  * is why they cannot both say something.
101
104
  */
102
- allowsDay(date) {
105
+ allowsDay(read) {
103
106
  if (this.fields.dayOfMonth.isAny) {
104
107
  // AWS numbers the week from Sunday as one, and JavaScript from Sunday as
105
108
  // zero.
106
- return this.fields.dayOfWeek.allows(date.getUTCDay() + 1);
109
+ return this.fields.dayOfWeek.allows(read.weekday + 1);
107
110
  }
108
- return this.fields.dayOfMonth.allows(date.getUTCDate());
111
+ return this.fields.dayOfMonth.allows(read.day);
109
112
  }
110
113
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The wall-clock reading of one instant, as a schedule expression names it.
3
+ */
4
+ export interface SimScheduleZoneParts {
5
+ readonly year: number;
6
+ /** The month as a JavaScript `Date` numbers it, January being zero. */
7
+ readonly month: number;
8
+ readonly day: number;
9
+ readonly hour: number;
10
+ readonly minute: number;
11
+ /** The day of the week, Sunday being zero. */
12
+ readonly weekday: number;
13
+ }
14
+ /**
15
+ * Reads what a clock in one zone says at an instant.
16
+ */
17
+ export type SimScheduleZoneClock = (instant: number) => SimScheduleZoneParts;
18
+ /**
19
+ * A clock for one zone, refusing a name no zone answers to.
20
+ *
21
+ * `Intl.DateTimeFormat` is what knows the offsets, including the two days a
22
+ * year a zone changes one, so no timezone database travels with this. Which
23
+ * names there are is therefore the host's own, and a name it has never heard
24
+ * of is refused here.
25
+ */
26
+ export declare function simScheduleZoneClock(name: string): SimScheduleZoneClock;