@kensio/yulin 1.21.21 → 1.21.23
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/service/athena/engine/sim-athena-ast-nodes.d.ts +11 -0
- package/dist/service/athena/engine/sim-athena-ast-nodes.js +22 -0
- package/dist/service/athena/engine/sim-athena-engine-result.d.ts +9 -1
- package/dist/service/athena/engine/sim-athena-engine-result.js +15 -4
- package/dist/service/athena/engine/sim-athena-engine-run.d.ts +2 -0
- package/dist/service/athena/engine/sim-athena-engine-run.js +6 -1
- package/dist/service/athena/engine/sim-athena-query-engine.js +1 -0
- package/dist/service/athena/engine/sim-athena-result-columns.d.ts +1 -1
- package/dist/service/athena/engine/sim-athena-result-columns.js +11 -4
- package/dist/service/athena/engine/sim-athena-round-scales.d.ts +19 -0
- package/dist/service/athena/engine/sim-athena-round-scales.js +80 -0
- package/dist/service/athena/engine/sim-athena-sql-translation.d.ts +9 -0
- package/dist/service/athena/engine/sim-athena-sql-translation.js +17 -2
- package/dist/service/athena/engine/sim-athena-string-shims.js +29 -1
- package/dist/service/athena/engine/sim-athena-unnest-item.js +6 -15
- package/dist/service/athena/engine/sim-athena-unnest-rewrite.d.ts +4 -4
- package/dist/service/athena/engine/sim-athena-unnest-rewrite.js +4 -4
- package/dist/service/athena/engine/sim-athena-unnest-source.d.ts +10 -9
- package/dist/service/athena/engine/sim-athena-unnest-source.js +25 -11
- package/dist/service/athena/sim-athena-flattened-events.fixture.d.ts +3 -0
- package/dist/service/athena/sim-athena-flattened-events.fixture.js +46 -0
- package/dist/service/lambda/cfn/event-source-mapping/sim-cfn-lambda-event-source-mapping-properties.js +2 -0
- package/dist/service/lambda/cfn/event-source-mapping/sim-cfn-lambda-event-source-mapping-property-names.js +1 -5
- package/dist/service/lambda/command/event-source-mapping/create-event-source-mapping-input.d.ts +5 -0
- package/dist/service/lambda/command/event-source-mapping/create-event-source-mapping-input.js +7 -0
- package/dist/service/lambda/command/event-source-mapping/create-event-source-mapping-refusals.js +0 -1
- package/dist/service/lambda/command/event-source-mapping/create-event-source-mapping.handler.js +1 -0
- package/dist/service/lambda/command/event-source-mapping/event-source-mapping.command.d.ts +2 -1
- package/dist/service/lambda/command/event-source-mapping/event-source-mapping.command.js +0 -6
- package/dist/service/lambda/event-source/filter/sim-lambda-filter-criteria.d.ts +48 -0
- package/dist/service/lambda/event-source/filter/sim-lambda-filter-criteria.js +129 -0
- package/dist/service/lambda/event-source/filter/sim-lambda-filtered-delivery.d.ts +27 -0
- package/dist/service/lambda/event-source/filter/sim-lambda-filtered-delivery.js +15 -0
- package/dist/service/lambda/event-source/filter/sim-lambda-filtered-records.d.ts +24 -0
- package/dist/service/lambda/event-source/filter/sim-lambda-filtered-records.js +35 -0
- package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-shard-poller.d.ts +7 -4
- package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-shard-poller.js +9 -6
- package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-stream-delivery.d.ts +16 -4
- package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-stream-delivery.js +36 -5
- package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-stream-event-source-poller.d.ts +4 -3
- package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-stream-event-source-poller.js +6 -7
- package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-stream-event.d.ts +14 -2
- package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-stream-event.js +21 -2
- package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-delivery.d.ts +19 -6
- package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-delivery.js +41 -7
- package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-event-source-poller.d.ts +7 -3
- package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-event-source-poller.js +10 -9
- package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-event.d.ts +6 -2
- package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-event.js +6 -2
- package/dist/service/lambda/event-source/poll/sim-lambda-event-source-delivery.d.ts +21 -1
- package/dist/service/lambda/event-source/poll/sim-lambda-event-source-delivery.js +32 -2
- package/dist/service/lambda/event-source/poll/sim-lambda-sqs-delivery.js +1 -0
- package/dist/service/lambda/event-source/poll/sim-lambda-sqs-event.d.ts +8 -0
- package/dist/service/lambda/event-source/poll/sim-lambda-sqs-event.js +11 -0
- package/dist/service/lambda/event-source/sim-lambda-event-source-mapping.d.ts +10 -0
- package/dist/service/lambda/event-source/sim-lambda-event-source-mapping.js +11 -0
- package/dist/service/lambda/event-source/stream/sim-lambda-stream-cascade-guard.d.ts +35 -8
- package/dist/service/lambda/event-source/stream/sim-lambda-stream-cascade-guard.js +35 -12
- package/dist/service/lambda/event-source/stream/sim-lambda-stream-cascade.error.d.ts +13 -4
- package/dist/service/lambda/event-source/stream/sim-lambda-stream-cascade.error.js +12 -6
- package/dist/service/lambda/event-source/stream/sim-lambda-stream-halt.d.ts +18 -0
- package/dist/service/lambda/event-source/stream/sim-lambda-stream-halt.js +22 -0
- package/dist/service/scheduler/command/schedule/schedule.command.d.ts +1 -0
- package/dist/service/scheduler/command/schedule/sim-scheduler-described-schedule.js +1 -0
- package/dist/service/scheduler/command/schedule/sim-scheduler-schedule-writer.js +2 -1
- package/dist/service/scheduler/command/schedule/sim-scheduler-unsimulated-input.d.ts +3 -3
- package/dist/service/scheduler/command/schedule/sim-scheduler-unsimulated-input.js +19 -11
- package/dist/service/scheduler/schedule/sim-scheduler-schedule-expression.d.ts +5 -3
- package/dist/service/scheduler/schedule/sim-scheduler-schedule-expression.js +6 -4
- package/dist/service/scheduler/schedule/sim-scheduler-schedule.d.ts +9 -0
- package/dist/service/scheduler/schedule/sim-scheduler-schedule.js +8 -0
- package/dist/util/schedule/at/sim-at-expression.d.ts +2 -1
- package/dist/util/schedule/at/sim-at-expression.js +7 -3
- package/dist/util/schedule/cron/sim-cron-expression.d.ts +6 -3
- package/dist/util/schedule/cron/sim-cron-expression.js +24 -21
- package/dist/util/schedule/sim-schedule-zone-clock.d.ts +26 -0
- package/dist/util/schedule/sim-schedule-zone-clock.js +56 -0
- package/dist/util/schedule/sim-schedule-zone.d.ts +58 -0
- package/dist/util/schedule/sim-schedule-zone.js +83 -0
- package/dist/util/schedule/sim-schedule.d.ts +1 -1
- package/dist/util/schedule/sim-schedule.js +6 -5
- package/docs/services/athena/README.md +26 -2
- package/docs/services/lambda/README.md +159 -15
- package/docs/services/scheduler/README.md +35 -6
- package/package.json +1 -1
|
@@ -1,51 +1,74 @@
|
|
|
1
1
|
import { simLambdaEventSourceDeliveryContext } from "./sim-lambda-event-source-delivery-context.js";
|
|
2
2
|
import { SimLambdaStreamCascadeError } from "./sim-lambda-stream-cascade.error.js";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* How many deliveries in a row may feed the source that invoked them before the
|
|
5
|
+
* simulation is refused.
|
|
6
|
+
*
|
|
7
|
+
* One write back is a handler doing its job, and the delivery it causes is the
|
|
8
|
+
* chance for that work to finish. A handler that settles takes two or three
|
|
9
|
+
* links of chain. A handler that loops takes every link there is, and the
|
|
10
|
+
* simulation has to say so before the test times out.
|
|
11
|
+
*/
|
|
12
|
+
export const simLambdaStreamCascadeLimit = 10;
|
|
13
|
+
/**
|
|
14
|
+
* Watches one mapping's deliveries for a chain of the function feeding the
|
|
5
15
|
* source whose stream invoked it.
|
|
6
16
|
*
|
|
7
17
|
* The delivery runs inside its own asynchronous context, so a record written by
|
|
8
18
|
* the handler is told apart from one written by anything else that happened to
|
|
9
19
|
* be running at the same time. That distinction is the whole point: several
|
|
10
20
|
* records written at once are an ordinary batch, and a handler feeding its own
|
|
11
|
-
* source is a
|
|
21
|
+
* source is the start of a chain.
|
|
22
|
+
*
|
|
23
|
+
* What the guard counts is the chain, and never the shape of one write. A
|
|
24
|
+
* handler that writes back and then finds its own work done writes nothing the
|
|
25
|
+
* second time round, the chain ends at one link, and the simulation settles. A
|
|
26
|
+
* handler that writes back every time takes the chain to
|
|
27
|
+
* `simLambdaStreamCascadeLimit` and is refused there.
|
|
12
28
|
*/
|
|
13
29
|
export class SimLambdaStreamCascadeGuard {
|
|
14
30
|
properties;
|
|
15
|
-
|
|
31
|
+
fedRecords = 0;
|
|
32
|
+
chained = 0;
|
|
16
33
|
constructor(properties) {
|
|
17
34
|
this.properties = properties;
|
|
18
35
|
}
|
|
19
36
|
/**
|
|
20
|
-
* Run one delivery, refusing afterwards if the
|
|
37
|
+
* Run one delivery, refusing afterwards if the chain has gone on long enough
|
|
38
|
+
* to say the simulation will never settle.
|
|
21
39
|
*
|
|
22
40
|
* The refusal comes after the delivery rather than during it, so the handler
|
|
23
|
-
* sees its own write succeed and the
|
|
41
|
+
* sees its own write succeed and the chain is reported to whoever is waiting
|
|
24
42
|
* for the simulation to settle.
|
|
25
43
|
*/
|
|
26
44
|
async around(delivery) {
|
|
45
|
+
const fedBefore = this.fedRecords;
|
|
27
46
|
const result = await simLambdaEventSourceDeliveryContext.run(this, delivery);
|
|
28
|
-
|
|
47
|
+
this.chained = this.fedRecords > fedBefore ? this.chained + 1 : 0;
|
|
48
|
+
if (this.chained >= simLambdaStreamCascadeLimit) {
|
|
29
49
|
const { mapping, source } = this.properties;
|
|
50
|
+
this.properties.halt.stop();
|
|
30
51
|
throw new SimLambdaStreamCascadeError({
|
|
31
52
|
functionName: mapping.functionName,
|
|
32
53
|
streamArn: source.streamArn,
|
|
33
54
|
wroteTo: source.wroteTo,
|
|
34
55
|
sourceRelation: source.sourceRelation,
|
|
35
56
|
advice: source.advice,
|
|
57
|
+
deliveries: this.chained,
|
|
36
58
|
});
|
|
37
59
|
}
|
|
38
60
|
return result;
|
|
39
61
|
}
|
|
40
62
|
/**
|
|
41
|
-
* Note a record written to the polled stream,
|
|
42
|
-
* mapping's own function
|
|
63
|
+
* Note a record written to the polled stream, which counts only while this
|
|
64
|
+
* mapping's own function is the thing that is running.
|
|
65
|
+
*
|
|
66
|
+
* Counted rather than flagged, so a delivery can tell whether the count moved
|
|
67
|
+
* while it ran without the flag having to be put back afterwards.
|
|
43
68
|
*/
|
|
44
69
|
noteRecordWritten() {
|
|
45
|
-
if (
|
|
46
|
-
|
|
70
|
+
if (simLambdaEventSourceDeliveryContext.isDelivering(this)) {
|
|
71
|
+
this.fedRecords += 1;
|
|
47
72
|
}
|
|
48
|
-
this.cascaded = true;
|
|
49
|
-
return true;
|
|
50
73
|
}
|
|
51
74
|
}
|
|
@@ -17,19 +17,28 @@ interface SimLambdaStreamCascadeErrorProperties {
|
|
|
17
17
|
* in a second table, and a Kinesis result belongs on a second stream.
|
|
18
18
|
*/
|
|
19
19
|
readonly advice: string;
|
|
20
|
+
/**
|
|
21
|
+
* How many deliveries in a row fed the source, which is the chain the guard
|
|
22
|
+
* counted before it gave up on the simulation settling.
|
|
23
|
+
*/
|
|
24
|
+
readonly deliveries: number;
|
|
20
25
|
}
|
|
21
26
|
/**
|
|
22
|
-
* A function
|
|
27
|
+
* A function feeding the source whose stream invoked it, delivery after
|
|
28
|
+
* delivery.
|
|
23
29
|
*
|
|
24
30
|
* Every one of those writes is another stream record, which is another
|
|
25
31
|
* delivery, which is another write. Real Lambda runs that loop for as long as
|
|
26
32
|
* the account is willing to pay for it. Nothing here stops on its own, so the
|
|
27
33
|
* simulation would go round until the test timed out with nothing to point at.
|
|
28
34
|
*
|
|
35
|
+
* A handler that writes back and then settles is left alone. This is the chain
|
|
36
|
+
* that carried on, counted by `SimLambdaStreamCascadeGuard` up to
|
|
37
|
+
* `simLambdaStreamCascadeLimit`.
|
|
38
|
+
*
|
|
29
39
|
* This is a simulator guard rather than AWS behaviour, and it is deliberately
|
|
30
|
-
* not silent: a projection or an aggregate belongs in a second
|
|
31
|
-
*
|
|
32
|
-
* loop.
|
|
40
|
+
* not silent: a projection or an aggregate that keeps going belongs in a second
|
|
41
|
+
* table or on a second stream.
|
|
33
42
|
*/
|
|
34
43
|
export declare class SimLambdaStreamCascadeError extends SimLambdaError {
|
|
35
44
|
readonly name = "SimLambdaStreamCascadeError";
|
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
import { SimLambdaError } from "../../error/sim-lambda.error.js";
|
|
2
2
|
/**
|
|
3
|
-
* A function
|
|
3
|
+
* A function feeding the source whose stream invoked it, delivery after
|
|
4
|
+
* delivery.
|
|
4
5
|
*
|
|
5
6
|
* Every one of those writes is another stream record, which is another
|
|
6
7
|
* delivery, which is another write. Real Lambda runs that loop for as long as
|
|
7
8
|
* the account is willing to pay for it. Nothing here stops on its own, so the
|
|
8
9
|
* simulation would go round until the test timed out with nothing to point at.
|
|
9
10
|
*
|
|
11
|
+
* A handler that writes back and then settles is left alone. This is the chain
|
|
12
|
+
* that carried on, counted by `SimLambdaStreamCascadeGuard` up to
|
|
13
|
+
* `simLambdaStreamCascadeLimit`.
|
|
14
|
+
*
|
|
10
15
|
* This is a simulator guard rather than AWS behaviour, and it is deliberately
|
|
11
|
-
* not silent: a projection or an aggregate belongs in a second
|
|
12
|
-
*
|
|
13
|
-
* loop.
|
|
16
|
+
* not silent: a projection or an aggregate that keeps going belongs in a second
|
|
17
|
+
* table or on a second stream.
|
|
14
18
|
*/
|
|
15
19
|
export class SimLambdaStreamCascadeError extends SimLambdaError {
|
|
16
20
|
name = "SimLambdaStreamCascadeError";
|
|
17
21
|
constructor(properties) {
|
|
18
22
|
super(`Function ${properties.functionName} ${properties.wroteTo} while ` +
|
|
19
23
|
`handling records from ${properties.sourceRelation} ` +
|
|
20
|
-
`${properties.streamArn}
|
|
21
|
-
`
|
|
24
|
+
`${properties.streamArn} on ${String(properties.deliveries)} ` +
|
|
25
|
+
`deliveries in a row. Each of those writes was delivered back to the ` +
|
|
26
|
+
`function and brought on the next one, and the simulation is still ` +
|
|
27
|
+
`going round. ${properties.advice}`);
|
|
22
28
|
}
|
|
23
29
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a mapping has finished polling, and the one place that says so.
|
|
3
|
+
*
|
|
4
|
+
* Two things finish a mapping. Deleting it stops the poller, and the cascade
|
|
5
|
+
* guard refusing a chain of the function's own writes stops it too. A poll
|
|
6
|
+
* reads one answer either way, and neither side has to know about the other.
|
|
7
|
+
*/
|
|
8
|
+
export declare class SimLambdaStreamHalt {
|
|
9
|
+
private halted;
|
|
10
|
+
/**
|
|
11
|
+
* Whether polling has finished.
|
|
12
|
+
*/
|
|
13
|
+
get stopped(): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Finish polling.
|
|
16
|
+
*/
|
|
17
|
+
stop(): void;
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a mapping has finished polling, and the one place that says so.
|
|
3
|
+
*
|
|
4
|
+
* Two things finish a mapping. Deleting it stops the poller, and the cascade
|
|
5
|
+
* guard refusing a chain of the function's own writes stops it too. A poll
|
|
6
|
+
* reads one answer either way, and neither side has to know about the other.
|
|
7
|
+
*/
|
|
8
|
+
export class SimLambdaStreamHalt {
|
|
9
|
+
halted = false;
|
|
10
|
+
/**
|
|
11
|
+
* Whether polling has finished.
|
|
12
|
+
*/
|
|
13
|
+
get stopped() {
|
|
14
|
+
return this.halted;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Finish polling.
|
|
18
|
+
*/
|
|
19
|
+
stop() {
|
|
20
|
+
this.halted = true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
6
|
+
* Refuse a timezone no zone answers to, naming the parameter it came in on.
|
|
6
7
|
*/
|
|
7
|
-
|
|
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
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
21
|
-
*
|
|
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
|
-
|
|
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 =
|
|
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
|
|
75
|
-
const year =
|
|
77
|
+
const read = this.zone.partsAt(at);
|
|
78
|
+
const { year, month, day, hour } = read;
|
|
76
79
|
if (!this.fields.year.allows(year)) {
|
|
77
|
-
return
|
|
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
|
|
83
|
+
return this.zone.instantOf(year, month + 1, 1);
|
|
82
84
|
}
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
89
|
+
return this.zone.instantOf(year, month, day, hour + 1);
|
|
90
90
|
}
|
|
91
|
-
if (!this.fields.minutes.allows(
|
|
91
|
+
if (!this.fields.minutes.allows(read.minute)) {
|
|
92
92
|
return at + millisecondsPerMinute;
|
|
93
93
|
}
|
|
94
|
-
|
|
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(
|
|
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(
|
|
109
|
+
return this.fields.dayOfWeek.allows(read.weekday + 1);
|
|
107
110
|
}
|
|
108
|
-
return this.fields.dayOfMonth.allows(
|
|
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;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { SimScheduleExpressionError } from "./sim-schedule.error.js";
|
|
2
|
+
const weekdays = [
|
|
3
|
+
"Sun",
|
|
4
|
+
"Mon",
|
|
5
|
+
"Tue",
|
|
6
|
+
"Wed",
|
|
7
|
+
"Thu",
|
|
8
|
+
"Fri",
|
|
9
|
+
"Sat",
|
|
10
|
+
];
|
|
11
|
+
/**
|
|
12
|
+
* A clock for one zone, refusing a name no zone answers to.
|
|
13
|
+
*
|
|
14
|
+
* `Intl.DateTimeFormat` is what knows the offsets, including the two days a
|
|
15
|
+
* year a zone changes one, so no timezone database travels with this. Which
|
|
16
|
+
* names there are is therefore the host's own, and a name it has never heard
|
|
17
|
+
* of is refused here.
|
|
18
|
+
*/
|
|
19
|
+
export function simScheduleZoneClock(name) {
|
|
20
|
+
const format = zoneFormat(name);
|
|
21
|
+
return (instant) => {
|
|
22
|
+
const parts = new Map(format
|
|
23
|
+
.formatToParts(new Date(instant))
|
|
24
|
+
.map((part) => [part.type, part.value]));
|
|
25
|
+
return {
|
|
26
|
+
year: Number(parts.get("year")),
|
|
27
|
+
month: Number(parts.get("month")) - 1,
|
|
28
|
+
day: Number(parts.get("day")),
|
|
29
|
+
// Midnight reads as 24 under some locales rather than as zero.
|
|
30
|
+
hour: Number(parts.get("hour")) % 24,
|
|
31
|
+
minute: Number(parts.get("minute")),
|
|
32
|
+
weekday: weekdays.indexOf(parts.get("weekday") ?? ""),
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The formatter for a zone, which is also what decides the name is one.
|
|
38
|
+
*/
|
|
39
|
+
function zoneFormat(name) {
|
|
40
|
+
try {
|
|
41
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
42
|
+
timeZone: name,
|
|
43
|
+
hourCycle: "h23",
|
|
44
|
+
year: "numeric",
|
|
45
|
+
month: "2-digit",
|
|
46
|
+
day: "2-digit",
|
|
47
|
+
hour: "2-digit",
|
|
48
|
+
minute: "2-digit",
|
|
49
|
+
weekday: "short",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
throw new SimScheduleExpressionError(`'${name}' is not a timezone. A timezone is an IANA name such as ` +
|
|
54
|
+
`'Europe/London', or 'UTC'`);
|
|
55
|
+
}
|
|
56
|
+
}
|