@kensio/yulin 1.21.31 → 1.21.33

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 (78) hide show
  1. package/dist/serve/message/sim-message-log-console.js +3 -3
  2. package/dist/service/apigateway/api/deployment/sim-rest-api-deployment.js +3 -3
  3. package/dist/service/apigateway/api/method/sim-rest-api-method.js +12 -12
  4. package/dist/service/apigateway/api/resource/sim-rest-api-resource.js +11 -9
  5. package/dist/service/apigateway/api/sim-rest-api-view.js +3 -3
  6. package/dist/service/apigateway/api/stage/sim-rest-api-stage.js +8 -10
  7. package/dist/service/apigatewayv2/api/integration/sim-http-api-integration.js +3 -3
  8. package/dist/service/apigatewayv2/api/route/sim-http-api-route-view.js +6 -6
  9. package/dist/service/apigatewayv2/api/sim-http-api-view.js +3 -3
  10. package/dist/service/apigatewayv2/api/stage/settings/sim-http-api-stage-route-settings.js +8 -7
  11. package/dist/service/apigatewayv2/api/stage/sim-http-api-stage.js +9 -9
  12. package/dist/service/athena/engine/sim-athena-partitioned-objects.js +7 -7
  13. package/dist/service/athena/sim-athena-engine.fixture.js +3 -0
  14. package/dist/service/athena/table/sim-athena-partition-filters.js +6 -5
  15. package/dist/service/backup/command/sim-backup-authorizer.js +5 -4
  16. package/dist/service/bedrock/response/sim-bedrock-declared-content.js +5 -10
  17. package/dist/service/cloudformation/stack/update/sim-cfn-stack-resource-changes.js +19 -10
  18. package/dist/service/cloudfront/cfn/response-headers-policy/sim-cfn-cf-rh-policy-security-values.js +2 -6
  19. package/dist/service/cognito/user-pool/trigger/sim-cognito-pre-sign-up-response.js +4 -7
  20. package/dist/service/dynamodb/command/transact/sim-dynamodb-transact-write.js +34 -25
  21. package/dist/service/dynamodb/secondary-index/sim-dynamodb-index-limits.js +5 -4
  22. package/dist/service/dynamodb/stream/sim-dynamodb-stream-shard-iterator.js +1 -3
  23. package/dist/service/dynamodb/time-to-live/sim-dynamodb-time-to-live.js +4 -3
  24. package/dist/service/glue/expression/sim-glue-partition-comparisons.js +1 -1
  25. package/dist/service/iam/authorize/context/sim-iam-auth-z-allow-requirement.js +6 -6
  26. package/dist/service/iam/authorize/match/condition/null/sim-iam-null.js +4 -1
  27. package/dist/service/kinesis/stream/sim-kinesis-shard-iterator.js +1 -3
  28. package/dist/service/lambda/command/create-function/create-function.handler.js +1 -0
  29. package/dist/service/lambda/destination/sim-lambda-destination-record.js +7 -7
  30. package/dist/service/lambda/function/environment/sim-lambda-environment-conflict.d.ts +22 -0
  31. package/dist/service/lambda/function/environment/sim-lambda-environment-conflict.js +33 -0
  32. package/dist/service/lambda/function/environment/sim-lambda-environment-conflicts.d.ts +53 -11
  33. package/dist/service/lambda/function/environment/sim-lambda-environment-conflicts.js +78 -24
  34. package/dist/service/lambda/function/policy/sim-lambda-permission.js +15 -14
  35. package/dist/service/lambda/function/sim-lambda-function.js +1 -0
  36. package/dist/service/lambda/function/sim-lambda-function.type.d.ts +8 -0
  37. package/dist/service/lambda/metric/sim-lambda-function-metrics.js +11 -9
  38. package/dist/service/lambda/sim-lambda-commands.js +3 -1
  39. package/dist/service/logs/metric/sim-logs-metric-filter.js +1 -8
  40. package/dist/service/route53/dns/answer/sim-route53-dns-zone-finder.js +5 -4
  41. package/dist/service/route53/resolve/sim-r53-zone-record-finder.js +8 -6
  42. package/dist/service/s3/bucket/notification/sim-s3-notification-filter.js +4 -7
  43. package/dist/service/s3/bucket/sim-s3-bucket.d.ts +8 -0
  44. package/dist/service/s3/bucket/sim-s3-bucket.js +11 -0
  45. package/dist/service/s3/bucket/sim-s3-key-writes.d.ts +16 -0
  46. package/dist/service/s3/bucket/sim-s3-key-writes.js +43 -0
  47. package/dist/service/s3/bucket/versioning/sim-s3-bucket-versioning.js +1 -3
  48. package/dist/service/s3/command/complete-multipart-upload/complete-multipart-upload.command.d.ts +2 -1
  49. package/dist/service/s3/command/complete-multipart-upload/complete-multipart-upload.handler.js +5 -1
  50. package/dist/service/s3/command/copy-object/copy-object-precondition.d.ts +23 -0
  51. package/dist/service/s3/command/copy-object/copy-object-precondition.js +34 -0
  52. package/dist/service/s3/command/copy-object/copy-object.command.d.ts +3 -1
  53. package/dist/service/s3/command/copy-object/copy-object.handler.d.ts +4 -0
  54. package/dist/service/s3/command/copy-object/copy-object.handler.js +11 -1
  55. package/dist/service/s3/command/object/sim-s3-write-precondition.d.ts +34 -0
  56. package/dist/service/s3/command/object/sim-s3-write-precondition.js +47 -0
  57. package/dist/service/s3/command/put-object/put-object.command.d.ts +2 -1
  58. package/dist/service/s3/command/put-object/put-object.handler.js +5 -1
  59. package/dist/service/s3/error/sim-s3.error.d.ts +10 -0
  60. package/dist/service/s3/error/sim-s3.error.js +12 -0
  61. package/dist/service/s3/object/s3-object-listing.js +4 -3
  62. package/dist/service/s3/serve/api/sim-s3-api-input.d.ts +4 -0
  63. package/dist/service/s3/serve/api/sim-s3-api-input.js +10 -0
  64. package/dist/service/s3/serve/api/sim-s3-api-multipart-input.js +2 -0
  65. package/dist/service/s3/serve/api/sim-s3-api-object-input.d.ts +6 -0
  66. package/dist/service/s3/serve/api/sim-s3-api-object-input.js +34 -2
  67. package/dist/service/s3/storage/filesystem/s3-filesystem-object-keys.js +9 -8
  68. package/dist/service/secretsmanager/cfn/secret/sim-cfn-secrets-manager-secret-template.test-support.js +15 -23
  69. package/dist/service/sqs/message/sim-sqs-message.js +3 -3
  70. package/dist/service/stepfunctions/machine/sim-state-machine.js +4 -3
  71. package/dist/util/process/sim-process-environment-reads.d.ts +52 -0
  72. package/dist/util/process/sim-process-environment-reads.js +94 -0
  73. package/dist/util/process/sim-process-environment.d.ts +32 -9
  74. package/dist/util/process/sim-process-environment.js +43 -17
  75. package/dist/util/schedule/cron/sim-cron-field-bounds.js +1 -1
  76. package/docs/services/lambda/README.md +6 -0
  77. package/docs/services/s3/README.md +84 -3
  78. package/package.json +3 -3
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The advice that follows each warning, given the name that was read early.
3
+ */
4
+ function moveTheReadAdvice(name) {
5
+ return (`Code read process.env.${name} outside any invocation, as a read at ` +
6
+ "module scope does when the module is imported, and saw the host value " +
7
+ "instead. Move the read inside the handler function.");
8
+ }
9
+ /**
10
+ * The conflict of a function declaring a name the host process also sets,
11
+ * with a different value.
12
+ */
13
+ export function simLambdaHostConflict(environment, name) {
14
+ return {
15
+ key: `host:${name}`,
16
+ name,
17
+ message: `Sim Lambda function "${environment.functionName}" declares ` +
18
+ `environment variable ${name}, which the host process also sets ` +
19
+ `with a different value. ${moveTheReadAdvice(name)}`,
20
+ };
21
+ }
22
+ /**
23
+ * The conflict of two functions declaring different values for one name.
24
+ */
25
+ export function simLambdaFunctionConflict(existing, environment, name) {
26
+ return {
27
+ key: `function:${name}`,
28
+ name,
29
+ message: `Sim Lambda functions "${existing.functionName}" and ` +
30
+ `"${environment.functionName}" declare different values for ` +
31
+ `environment variable ${name}. ${moveTheReadAdvice(name)}`,
32
+ };
33
+ }
@@ -1,15 +1,32 @@
1
1
  import type { SimLambdaEnvironment } from "./sim-lambda-environment.js";
2
+ interface SimLambdaEnvironmentConflictsProperties {
3
+ /**
4
+ * The environments of the functions the simulated Lambda holds when it is
5
+ * asked, which a held name is checked against again once it is read. A
6
+ * check built standalone has none.
7
+ */
8
+ readonly currentEnvironments?: () => Iterable<SimLambdaEnvironment>;
9
+ }
2
10
  /**
3
11
  * Warns about declared environment variables whose value a handler module
4
- * might not actually see.
12
+ * did not actually see.
5
13
  *
6
14
  * A sim Lambda function backed by a real in-process handler only gets its own
7
15
  * process.env while the handler runs, so a variable read at module scope is
8
16
  * read too early (see SimLambdaProcessEnv). That only changes what the code
9
- * sees when the value read at module scope differs from the declared one, so
10
- * these are the two conditions worth reporting: the host process sets the
11
- * same name with a different value, or another simulated function declares
12
- * the same name with a different value.
17
+ * sees when the value read at module scope differs from the declared one.
18
+ * There are two ways for it to differ. The host process sets the same name
19
+ * with a different value, or another simulated function declares the same
20
+ * name with a different value.
21
+ *
22
+ * A difference is reported only once code has read the name from process.env
23
+ * outside any invocation, which SimProcessEnvironment records. The name of a
24
+ * difference with no such read yet is held. If the read turns up before a
25
+ * later invocation (a handler module imported after the function was
26
+ * created), the name is checked again against the functions that exist then,
27
+ * so a function deleted or reconfigured in between leaves nothing stale to
28
+ * report. A read made before Yulin was imported is not on record, and leaves
29
+ * the difference unreported.
13
30
  *
14
31
  * Staying quiet when the values agree matters as much as warning when they do
15
32
  * not. Test suites commonly export the same variables they configure their
@@ -18,30 +35,55 @@ import type { SimLambdaEnvironment } from "./sim-lambda-environment.js";
18
35
  */
19
36
  export declare class SimLambdaEnvironmentConflicts {
20
37
  /**
21
- * Variable names already reported, so repeated function creation does not
38
+ * The environments of the functions that exist now.
39
+ */
40
+ private readonly currentEnvironments;
41
+ /**
42
+ * Conflict keys already reported, so repeated function creation does not
22
43
  * repeat the same warning.
23
44
  */
24
45
  private readonly warned;
46
+ /**
47
+ * Names in conflict that had not been read outside an invocation when the
48
+ * conflict was found.
49
+ */
50
+ private readonly held;
51
+ constructor(properties?: SimLambdaEnvironmentConflictsProperties);
25
52
  /**
26
53
  * Check a new function's environment against the host process and against
27
54
  * the functions that already exist.
28
55
  */
29
56
  check(environment: SimLambdaEnvironment, existingEnvironments: Iterable<SimLambdaEnvironment>): void;
30
57
  /**
31
- * Warn when the host process sets the same name with a different value.
58
+ * Check the held names that have since been read outside an invocation
59
+ * again, against the functions that exist now, and report what conflicts.
60
+ *
61
+ * Called as a function is invoked, before the handler runs.
32
62
  */
33
- private checkHostProcess;
63
+ reportReads(): void;
64
+ /**
65
+ * Check one name across the given environments, each against the host
66
+ * process and against the environments before it.
67
+ */
68
+ private checkName;
34
69
  /**
35
- * Warn when another simulated function declares the same name with a
70
+ * Find a conflict when the host process sets the same name with a
36
71
  * different value.
37
72
  */
73
+ private checkHostProcess;
74
+ /**
75
+ * Find a conflict when another simulated function declares the same name
76
+ * with a different value.
77
+ */
38
78
  private checkOtherFunctions;
39
79
  /**
40
- * Report a conflict once per variable name.
80
+ * Warn about a conflict once per key if its name has been read outside an
81
+ * invocation, and hold the name otherwise.
41
82
  *
42
83
  * Warnings go to the console because that is where a test runner surfaces
43
84
  * them next to the failing expectation they explain; the simulator has no
44
85
  * logger of its own to route them through.
45
86
  */
46
- private warn;
87
+ private report;
47
88
  }
89
+ export {};
@@ -1,18 +1,25 @@
1
+ import { simLambdaFunctionConflict, simLambdaHostConflict, } from "./sim-lambda-environment-conflict.js";
1
2
  import { simProcessEnvironment } from "../../../../util/process/sim-process-environment.js";
2
- const moveTheReadAdvice = "A read of process.env at module scope happens when the module is " +
3
- "imported, before any invocation, so it sees the host value instead. " +
4
- "Move the read inside the handler function.";
5
3
  /**
6
4
  * Warns about declared environment variables whose value a handler module
7
- * might not actually see.
5
+ * did not actually see.
8
6
  *
9
7
  * A sim Lambda function backed by a real in-process handler only gets its own
10
8
  * process.env while the handler runs, so a variable read at module scope is
11
9
  * read too early (see SimLambdaProcessEnv). That only changes what the code
12
- * sees when the value read at module scope differs from the declared one, so
13
- * these are the two conditions worth reporting: the host process sets the
14
- * same name with a different value, or another simulated function declares
15
- * the same name with a different value.
10
+ * sees when the value read at module scope differs from the declared one.
11
+ * There are two ways for it to differ. The host process sets the same name
12
+ * with a different value, or another simulated function declares the same
13
+ * name with a different value.
14
+ *
15
+ * A difference is reported only once code has read the name from process.env
16
+ * outside any invocation, which SimProcessEnvironment records. The name of a
17
+ * difference with no such read yet is held. If the read turns up before a
18
+ * later invocation (a handler module imported after the function was
19
+ * created), the name is checked again against the functions that exist then,
20
+ * so a function deleted or reconfigured in between leaves nothing stale to
21
+ * report. A read made before Yulin was imported is not on record, and leaves
22
+ * the difference unreported.
16
23
  *
17
24
  * Staying quiet when the values agree matters as much as warning when they do
18
25
  * not. Test suites commonly export the same variables they configure their
@@ -21,22 +28,68 @@ const moveTheReadAdvice = "A read of process.env at module scope happens when th
21
28
  */
22
29
  export class SimLambdaEnvironmentConflicts {
23
30
  /**
24
- * Variable names already reported, so repeated function creation does not
31
+ * The environments of the functions that exist now.
32
+ */
33
+ currentEnvironments;
34
+ /**
35
+ * Conflict keys already reported, so repeated function creation does not
25
36
  * repeat the same warning.
26
37
  */
27
38
  warned = new Set();
39
+ /**
40
+ * Names in conflict that had not been read outside an invocation when the
41
+ * conflict was found.
42
+ */
43
+ held = new Set();
44
+ constructor(properties = {}) {
45
+ this.currentEnvironments =
46
+ properties.currentEnvironments ?? (() => []);
47
+ }
28
48
  /**
29
49
  * Check a new function's environment against the host process and against
30
50
  * the functions that already exist.
31
51
  */
32
52
  check(environment, existingEnvironments) {
53
+ const existing = [...existingEnvironments];
33
54
  for (const [name, value] of environment.declaredVariables) {
34
55
  this.checkHostProcess(environment, name, value);
35
- this.checkOtherFunctions(environment, name, value, existingEnvironments);
56
+ this.checkOtherFunctions(environment, name, value, existing);
57
+ }
58
+ }
59
+ /**
60
+ * Check the held names that have since been read outside an invocation
61
+ * again, against the functions that exist now, and report what conflicts.
62
+ *
63
+ * Called as a function is invoked, before the handler runs.
64
+ */
65
+ reportReads() {
66
+ const read = [...this.held].filter((name) => simProcessEnvironment.wasReadOutsideRun(name));
67
+ if (read.length === 0) {
68
+ return;
69
+ }
70
+ const environments = [...this.currentEnvironments()];
71
+ for (const name of read) {
72
+ this.held.delete(name);
73
+ this.checkName(name, environments);
74
+ }
75
+ }
76
+ /**
77
+ * Check one name across the given environments, each against the host
78
+ * process and against the environments before it.
79
+ */
80
+ checkName(name, environments) {
81
+ for (const [index, environment] of environments.entries()) {
82
+ const value = environment.declaredVariables.get(name);
83
+ if (value === undefined) {
84
+ continue;
85
+ }
86
+ this.checkHostProcess(environment, name, value);
87
+ this.checkOtherFunctions(environment, name, value, environments.slice(0, index));
36
88
  }
37
89
  }
38
90
  /**
39
- * Warn when the host process sets the same name with a different value.
91
+ * Find a conflict when the host process sets the same name with a
92
+ * different value.
40
93
  */
41
94
  checkHostProcess(environment, name, value) {
42
95
  // Read-only lookup of a name the function itself declared.
@@ -45,13 +98,11 @@ export class SimLambdaEnvironmentConflicts {
45
98
  if (hostValue === undefined || hostValue === value) {
46
99
  return;
47
100
  }
48
- this.warn(`host:${name}`, `Sim Lambda function "${environment.functionName}" declares ` +
49
- `environment variable ${name}, which the host process also sets ` +
50
- `with a different value. ${moveTheReadAdvice}`);
101
+ this.report(simLambdaHostConflict(environment, name));
51
102
  }
52
103
  /**
53
- * Warn when another simulated function declares the same name with a
54
- * different value.
104
+ * Find a conflict when another simulated function declares the same name
105
+ * with a different value.
55
106
  */
56
107
  checkOtherFunctions(environment, name, value, existingEnvironments) {
57
108
  for (const existing of existingEnvironments) {
@@ -59,25 +110,28 @@ export class SimLambdaEnvironmentConflicts {
59
110
  if (existingValue === undefined || existingValue === value) {
60
111
  continue;
61
112
  }
62
- this.warn(`function:${name}`, `Sim Lambda functions "${existing.functionName}" and ` +
63
- `"${environment.functionName}" declare different values for ` +
64
- `environment variable ${name}. ${moveTheReadAdvice}`);
113
+ this.report(simLambdaFunctionConflict(existing, environment, name));
65
114
  return;
66
115
  }
67
116
  }
68
117
  /**
69
- * Report a conflict once per variable name.
118
+ * Warn about a conflict once per key if its name has been read outside an
119
+ * invocation, and hold the name otherwise.
70
120
  *
71
121
  * Warnings go to the console because that is where a test runner surfaces
72
122
  * them next to the failing expectation they explain; the simulator has no
73
123
  * logger of its own to route them through.
74
124
  */
75
- warn(key, message) {
76
- if (this.warned.has(key)) {
125
+ report(conflict) {
126
+ if (this.warned.has(conflict.key)) {
127
+ return;
128
+ }
129
+ if (!simProcessEnvironment.wasReadOutsideRun(conflict.name)) {
130
+ this.held.add(conflict.name);
77
131
  return;
78
132
  }
79
- this.warned.add(key);
133
+ this.warned.add(conflict.key);
80
134
  // oxlint-disable-next-line no-console
81
- console.warn(message);
135
+ console.warn(conflict.message);
82
136
  }
83
137
  }
@@ -82,20 +82,21 @@ export class SimLambdaPermission {
82
82
  */
83
83
  condition() {
84
84
  const stringEquals = this.stringEqualsConditions();
85
- const condition = {};
86
- if (Object.keys(stringEquals).length > 0) {
87
- condition["StringEquals"] = stringEquals;
88
- }
89
- if (this.sourceArn !== undefined) {
90
- condition["ArnLike"] = {
91
- [simLambdaSourceArnConditionKey]: this.sourceArn,
92
- };
93
- }
94
- if (this.invokedViaFunctionUrl !== undefined) {
95
- condition["Bool"] = {
96
- [simLambdaInvokedViaFunctionUrlConditionKey]: this.invokedViaFunctionUrl,
97
- };
98
- }
85
+ const condition = {
86
+ ...(Object.keys(stringEquals).length > 0 && {
87
+ StringEquals: stringEquals,
88
+ }),
89
+ ...(this.sourceArn !== undefined && {
90
+ ArnLike: {
91
+ [simLambdaSourceArnConditionKey]: this.sourceArn,
92
+ },
93
+ }),
94
+ ...(this.invokedViaFunctionUrl !== undefined && {
95
+ Bool: {
96
+ [simLambdaInvokedViaFunctionUrlConditionKey]: this.invokedViaFunctionUrl,
97
+ },
98
+ }),
99
+ };
99
100
  return Object.keys(condition).length === 0 ? undefined : condition;
100
101
  }
101
102
  stringEqualsConditions() {
@@ -194,6 +194,7 @@ export class SimLambdaFunction {
194
194
  logStreamName: this.logging.logStreamName(),
195
195
  });
196
196
  this.logging.recordFrom(this.#code);
197
+ this.properties.environmentConflicts?.reportReads();
197
198
  return await this.metrics.around(String(this.name), async () => simAwsRunAsContext.run(this.runAsOwner, { kind: "arn", arn: this.roleArn }, async () => await this.environment.runWith(async () => await this.runInHostScope(async () => await this.logging.around(async () => await invocation.run(this.#code.handlerFunction(), event))))));
198
199
  }
199
200
  /**
@@ -8,6 +8,7 @@ import type { SimLambdaOutput } from "./logging/sim-lambda-output.js";
8
8
  import type { SimLogsServiceWriter } from "../../logs/write/sim-logs-service-writer.js";
9
9
  import type { SimLambdaExecutableCode } from "./code/sim-lambda-executable-code.js";
10
10
  import type { SimLambdaEnvironment } from "./environment/sim-lambda-environment.js";
11
+ import type { SimLambdaEnvironmentConflicts } from "./environment/sim-lambda-environment-conflicts.js";
11
12
  import type { SimLambdaOutboundHttp } from "./outbound/sim-lambda-outbound-http.js";
12
13
  import type { SimLambdaFunctionState } from "./sim-lambda-function-configuration.js";
13
14
  import type { SimLambdaHandler } from "./sim-lambda-handler.type.js";
@@ -27,6 +28,13 @@ export interface SimLambdaFunctionProperties {
27
28
  timeoutSeconds?: number | undefined;
28
29
  memorySizeMb?: number | undefined;
29
30
  environment?: SimLambdaEnvironment | undefined;
31
+ /**
32
+ * The check that warns about declared variables read too early, shared with
33
+ * every other function of the same simulated Lambda. It reports, as this
34
+ * function is invoked, a difference found earlier whose name has since been
35
+ * read outside an invocation. A function built standalone has none.
36
+ */
37
+ environmentConflicts?: SimLambdaEnvironmentConflicts | undefined;
30
38
  /**
31
39
  * The queue or topic this function's abandoned asynchronous events are sent
32
40
  * to, which is what `DeadLetterConfig.TargetArn` names.
@@ -69,15 +69,17 @@ function invocationData(outcome) {
69
69
  value: outcome.durationMilliseconds,
70
70
  unit: "Milliseconds",
71
71
  },
72
+ ...(outcome.failed
73
+ ? [
74
+ {
75
+ namespace: simLambdaMetricNamespace,
76
+ metricName: "Errors",
77
+ dimensions,
78
+ value: 1,
79
+ unit: "Count",
80
+ },
81
+ ]
82
+ : []),
72
83
  ];
73
- if (outcome.failed) {
74
- data.push({
75
- namespace: simLambdaMetricNamespace,
76
- metricName: "Errors",
77
- dimensions,
78
- value: 1,
79
- unit: "Count",
80
- });
81
- }
82
84
  return data;
83
85
  }
@@ -146,7 +146,9 @@ export class SimLambdaCommands {
146
146
  runAsOwner: properties.runAsOwner,
147
147
  // Shared across function creations so a conflicting environment variable
148
148
  // is only reported once for this simulated Lambda.
149
- environmentConflicts: new SimLambdaEnvironmentConflicts(),
149
+ environmentConflicts: new SimLambdaEnvironmentConflicts({
150
+ currentEnvironments: () => properties.functions.values().map((existing) => existing.environment),
151
+ }),
150
152
  eventInvokeConfigs: this.eventInvokeConfigStore,
151
153
  destinations,
152
154
  codeStore,
@@ -84,12 +84,5 @@ export class SimLogsMetricFilter {
84
84
  * A batch's events grouped into the aggregation periods they were taken in.
85
85
  */
86
86
  function byPeriod(events) {
87
- const grouped = new Map();
88
- for (const event of events) {
89
- const period = Math.floor(event.ingestionTime / periodMilliseconds) * periodMilliseconds;
90
- const inPeriod = grouped.get(period) ?? [];
91
- inPeriod.push(event);
92
- grouped.set(period, inPeriod);
93
- }
94
- return grouped;
87
+ return Map.groupBy(events, (event) => Math.floor(event.ingestionTime / periodMilliseconds) * periodMilliseconds);
95
88
  }
@@ -19,11 +19,12 @@ export class SimRoute53DnsZoneFinder {
19
19
  let bestZoneNameLength = -1;
20
20
  for (const hostedZone of this.hostedZones.values()) {
21
21
  const zoneName = simRoute53ZoneName(hostedZone);
22
- if (zoneContainsName(zoneName, name) &&
23
- zoneName.length > bestZoneNameLength) {
24
- bestZone = hostedZone;
25
- bestZoneNameLength = zoneName.length;
22
+ if (!zoneContainsName(zoneName, name) ||
23
+ zoneName.length <= bestZoneNameLength) {
24
+ continue;
26
25
  }
26
+ bestZone = hostedZone;
27
+ bestZoneNameLength = zoneName.length;
27
28
  }
28
29
  return bestZone;
29
30
  }
@@ -20,10 +20,11 @@ export class SimRoute53HostedZoneRecordFinder {
20
20
  continue;
21
21
  }
22
22
  const record = hostedZone.records.get(name, type);
23
- if (record !== undefined && zoneName.length > bestZoneNameLength) {
24
- bestRecord = record;
25
- bestZoneNameLength = zoneName.length;
23
+ if (record === undefined || zoneName.length <= bestZoneNameLength) {
24
+ continue;
26
25
  }
26
+ bestRecord = record;
27
+ bestZoneNameLength = zoneName.length;
27
28
  }
28
29
  return bestRecord;
29
30
  }
@@ -43,10 +44,11 @@ export class SimRoute53HostedZoneRecordFinder {
43
44
  if (!this.zoneContainsName(zoneName, name)) {
44
45
  continue;
45
46
  }
46
- if (zoneName.length > bestZoneNameLength) {
47
- bestHostedZone = hostedZone;
48
- bestZoneNameLength = zoneName.length;
47
+ if (zoneName.length <= bestZoneNameLength) {
48
+ continue;
49
49
  }
50
+ bestHostedZone = hostedZone;
51
+ bestZoneNameLength = zoneName.length;
50
52
  }
51
53
  return bestHostedZone;
52
54
  }
@@ -55,13 +55,10 @@ export class SimS3NotificationFilter {
55
55
  * filters nothing.
56
56
  */
57
57
  toOutput() {
58
- const rules = [];
59
- if (this.prefix !== "") {
60
- rules.push({ Name: "prefix", Value: this.prefix });
61
- }
62
- if (this.suffix !== "") {
63
- rules.push({ Name: "suffix", Value: this.suffix });
64
- }
58
+ const rules = [
59
+ ...(this.prefix === "" ? [] : [{ Name: "prefix", Value: this.prefix }]),
60
+ ...(this.suffix === "" ? [] : [{ Name: "suffix", Value: this.suffix }]),
61
+ ];
65
62
  if (rules.length === 0) {
66
63
  return undefined;
67
64
  }
@@ -23,9 +23,17 @@ export declare class SimS3Bucket extends SimS3BucketConfiguration {
23
23
  private readonly accountRegionScope;
24
24
  private readonly systemMetadata;
25
25
  private readonly objects;
26
+ private readonly keyWrites;
26
27
  constructor(properties: SimS3BucketProperties);
27
28
  /** Put an Object into storage, answering the version it was given, if any. */
28
29
  putObject(object: SimS3Object): Promise<SimS3ObjectVersion | undefined>;
30
+ /**
31
+ * Run a write to a key once every earlier write to it has finished.
32
+ *
33
+ * A write that checks the key before replacing it does both inside this,
34
+ * so no other write to the key lands between the check and the put.
35
+ */
36
+ writeKey<T>(key: string, write: () => Promise<T>): Promise<T>;
29
37
  /** Get a simulated S3 Object from storage. */
30
38
  getObject(key: string): Promise<SimS3Object | undefined>;
31
39
  /** List simulated S3 Objects from storage. */
@@ -1,6 +1,7 @@
1
1
  import { SimRealClock } from "../../../util/clock/sim-clock.js";
2
2
  import { MemoryS3BucketStorage } from "../storage/s3-memory-storage.js";
3
3
  import { SimS3BucketObjects } from "./sim-s3-bucket-objects.js";
4
+ import { SimS3KeyWrites } from "./sim-s3-key-writes.js";
4
5
  import { SimS3LifecycleConfiguration } from "./lifecycle/sim-s3-lifecycle-configuration.js";
5
6
  import { simS3BucketWebsiteUrl } from "./website/sim-s3-bucket-website-url.js";
6
7
  import { validateS3BucketName } from "./validate/validate-s3-bucket-name.js";
@@ -16,6 +17,7 @@ export class SimS3Bucket extends SimS3BucketConfiguration {
16
17
  accountRegionScope;
17
18
  systemMetadata = new SimS3BucketSystemMetadata();
18
19
  objects;
20
+ keyWrites = new SimS3KeyWrites();
19
21
  constructor(properties) {
20
22
  super(properties);
21
23
  const { bucketName, accountRegionScope = simAwsAccountRegionScopeFactory.make(), storage = new MemoryS3BucketStorage(), lifecycle = SimS3LifecycleConfiguration.empty(), clock = new SimRealClock(), creationDate = clock.now(), } = properties;
@@ -29,6 +31,15 @@ export class SimS3Bucket extends SimS3BucketConfiguration {
29
31
  async putObject(object) {
30
32
  return await this.objects.put(object);
31
33
  }
34
+ /**
35
+ * Run a write to a key once every earlier write to it has finished.
36
+ *
37
+ * A write that checks the key before replacing it does both inside this,
38
+ * so no other write to the key lands between the check and the put.
39
+ */
40
+ async writeKey(key, write) {
41
+ return await this.keyWrites.exclusive(key, write);
42
+ }
32
43
  /** Get a simulated S3 Object from storage. */
33
44
  async getObject(key) {
34
45
  return await this.objects.get(key);
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The writes in progress against each key of a Bucket, run one at a time.
3
+ *
4
+ * A conditional write reads the key and then writes it, with awaits between
5
+ * the two. Real S3 lets the first of several conditional writes to one key
6
+ * finish and refuses the rest. Running each key's writes in turn gives the
7
+ * same answer, because a write's condition is read after the write before it
8
+ * has landed. Writes to different keys still run side by side.
9
+ */
10
+ export declare class SimS3KeyWrites {
11
+ private readonly pending;
12
+ /**
13
+ * Run a write once every earlier write to the same key has settled.
14
+ */
15
+ exclusive<T>(key: string, write: () => Promise<T>): Promise<T>;
16
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The writes in progress against each key of a Bucket, run one at a time.
3
+ *
4
+ * A conditional write reads the key and then writes it, with awaits between
5
+ * the two. Real S3 lets the first of several conditional writes to one key
6
+ * finish and refuses the rest. Running each key's writes in turn gives the
7
+ * same answer, because a write's condition is read after the write before it
8
+ * has landed. Writes to different keys still run side by side.
9
+ */
10
+ export class SimS3KeyWrites {
11
+ pending = new Map();
12
+ /**
13
+ * Run a write once every earlier write to the same key has settled.
14
+ */
15
+ async exclusive(key, write) {
16
+ const running = afterEarlier(this.pending.get(key), write);
17
+ const settled = settle(running);
18
+ this.pending.set(key, settled);
19
+ try {
20
+ return await running;
21
+ }
22
+ finally {
23
+ if (this.pending.get(key) === settled) {
24
+ this.pending.delete(key);
25
+ }
26
+ }
27
+ }
28
+ }
29
+ async function afterEarlier(earlier, write) {
30
+ await earlier;
31
+ return await write();
32
+ }
33
+ /**
34
+ * Wait for a write without taking on its failure, which its own caller sees.
35
+ */
36
+ async function settle(running) {
37
+ try {
38
+ await running;
39
+ }
40
+ catch {
41
+ // The next write to the key runs whether this one failed or not.
42
+ }
43
+ }
@@ -34,9 +34,7 @@ export class SimS3BucketVersioning {
34
34
  * The state a PutBucketVersioning request asks for.
35
35
  */
36
36
  static fromConfiguration(configuration) {
37
- return configuration.Status === "Enabled"
38
- ? new SimS3BucketVersioning("Enabled")
39
- : new SimS3BucketVersioning("Suspended");
37
+ return new SimS3BucketVersioning(configuration.Status === "Enabled" ? "Enabled" : "Suspended");
40
38
  }
41
39
  /**
42
40
  * What GetBucketVersioning answers with.
@@ -1,4 +1,5 @@
1
1
  import type { SimResponseMetadata } from "../../../aws/metadata/response-metadata.type.js";
2
+ import type { SimS3WritePrecondition } from "../object/sim-s3-write-precondition.js";
2
3
  /**
3
4
  * Minimal structural sim S3 CompleteMultipartUpload command.
4
5
  */
@@ -18,7 +19,7 @@ export interface SimCompletedUploadPart {
18
19
  * The parts are named by the request rather than assumed by S3, so a client can
19
20
  * finish an upload it sent a part of twice.
20
21
  */
21
- export interface SimCompleteMultipartUploadCommandInput {
22
+ export interface SimCompleteMultipartUploadCommandInput extends SimS3WritePrecondition {
22
23
  readonly Bucket?: string | undefined;
23
24
  readonly Key?: string | undefined;
24
25
  readonly UploadId?: string | undefined;
@@ -3,6 +3,7 @@ import { simS3BucketUrl } from "../../bucket/sim-s3-endpoint-url.js";
3
3
  import { simS3QuotedETag } from "../../object/s3-object-etag.js";
4
4
  import { simS3CompletedUploadObject, } from "../../upload/sim-s3-completed-upload.js";
5
5
  import { SimS3MultipartAccess, } from "../multipart/sim-s3-multipart-access.js";
6
+ import { simS3RequireWritePrecondition } from "../object/sim-s3-write-precondition.js";
6
7
  /**
7
8
  * Simulated S3 CompleteMultipartUploadCommand handler.
8
9
  *
@@ -34,7 +35,10 @@ export class CompleteMultipartUploadCommandHandler {
34
35
  parts: namedParts(command.input.MultipartUpload?.Parts ?? []),
35
36
  completedAt: this.access.now(),
36
37
  });
37
- const version = await bucket.putObject(object);
38
+ const version = await bucket.writeKey(Key, async () => {
39
+ await simS3RequireWritePrecondition(bucket, Key, command.input);
40
+ return await bucket.putObject(object);
41
+ });
38
42
  bucket.getMultipartUploads().discard(UploadId);
39
43
  this.notifications.objectCreated({
40
44
  bucket,