sst 2.7.1 → 2.7.2

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.
@@ -0,0 +1,4 @@
1
+ export declare function getCiInfo(): {
2
+ isCI: boolean;
3
+ name: string | null;
4
+ };
package/cli/ci-info.js ADDED
@@ -0,0 +1,8 @@
1
+ import ciInfo from "ci-info";
2
+ export function getCiInfo() {
3
+ const isSeed = !!process.env.SEED_APP_NAME;
4
+ return {
5
+ isCI: ciInfo.isCI || isSeed,
6
+ name: (isSeed ? "Seed" : ciInfo.name) || null,
7
+ };
8
+ }
@@ -17,6 +17,7 @@ export const deploy = (program) => program.command("deploy [filter]", "Deploy yo
17
17
  const { dim, blue, bold } = await import("colorette");
18
18
  const { useProject } = await import("../../project.js");
19
19
  const { loadAssembly, useAppMetadata, saveAppMetadata, Stacks } = await import("../../stacks/index.js");
20
+ const { getCiInfo } = await import("../ci-info.js");
20
21
  const { render } = await import("ink");
21
22
  const { DeploymentUI } = await import("../ui/deploy.js");
22
23
  const { mapValues } = await import("remeda");
@@ -40,7 +41,7 @@ export const deploy = (program) => program.command("deploy [filter]", "Deploy yo
40
41
  });
41
42
  }
42
43
  // Check app mode changed
43
- if (appMetadata && appMetadata.mode !== "deploy") {
44
+ if (!getCiInfo().isCI && appMetadata && appMetadata.mode !== "deploy") {
44
45
  if (!(await promptChangeMode())) {
45
46
  process.exit(0);
46
47
  }
@@ -31,6 +31,7 @@ export const dev = (program) => program.command(["dev", "start"], "Work on your
31
31
  const { useMetadata } = await import("../../stacks/metadata.js");
32
32
  const { useIOT } = await import("../../iot.js");
33
33
  const { clear } = await import("../terminal.js");
34
+ const { getCiInfo } = await import("../ci-info.js");
34
35
  if (args._[0] === "start") {
35
36
  console.log(yellow(`Warning: ${bold(`sst start`)} has been renamed to ${bold(`sst dev`)}`));
36
37
  }
@@ -69,12 +70,16 @@ export const dev = (program) => program.command(["dev", "start"], "Work on your
69
70
  });
70
71
  bus.subscribe("function.build.success", async (evt) => {
71
72
  const info = useFunctions().fromID(evt.properties.functionID);
73
+ if (!info)
74
+ return;
72
75
  if (info.enableLiveDev === false)
73
76
  return;
74
77
  Colors.line(Colors.dim(Colors.prefix, "Built", info.handler));
75
78
  });
76
79
  bus.subscribe("function.build.failed", async (evt) => {
77
80
  const info = useFunctions().fromID(evt.properties.functionID);
81
+ if (!info)
82
+ return;
78
83
  if (info.enableLiveDev === false)
79
84
  return;
80
85
  Colors.gap();
@@ -109,7 +114,6 @@ export const dev = (program) => program.command(["dev", "start"], "Work on your
109
114
  const useStackBuilder = Context.memo(async () => {
110
115
  const watcher = useWatcher();
111
116
  const project = useProject();
112
- const bus = useBus();
113
117
  let lastDeployed;
114
118
  let isWorking = false;
115
119
  let isDirty = false;
@@ -276,7 +280,7 @@ export const dev = (program) => program.command(["dev", "start"], "Work on your
276
280
  });
277
281
  }
278
282
  // Check app mode changed
279
- if (appMetadata && appMetadata.mode !== "dev") {
283
+ if (!getCiInfo().isCI && appMetadata && appMetadata.mode !== "dev") {
280
284
  if (!(await promptChangeMode())) {
281
285
  process.exit(0);
282
286
  }
@@ -8,7 +8,6 @@ export const diff = (program) => program.command("diff", "Compare your app with
8
8
  const { useAWSClient } = await import("../../credentials.js");
9
9
  const { CloudFormationClient, GetTemplateCommand } = await import("@aws-sdk/client-cloudformation");
10
10
  const { createSpinner } = await import("../spinner.js");
11
- const { green } = await import("colorette");
12
11
  const { Colors } = await import("../colors.js");
13
12
  // Build app
14
13
  const project = useProject();
@@ -23,10 +22,13 @@ export const diff = (program) => program.command("diff", "Compare your app with
23
22
  for (const stack of assembly.stacks) {
24
23
  const spinner = createSpinner(`${stack.stackName}: Checking for changes...`);
25
24
  // get old template
26
- const response = await cfn.send(new GetTemplateCommand({
27
- StackName: stack.stackName,
28
- }));
29
- const oldTemplate = JSON.parse(response.TemplateBody);
25
+ const oldTemplate = await getTemplate(stack.stackName);
26
+ if (!oldTemplate) {
27
+ spinner.clear();
28
+ Colors.line(`➜ ${Colors.dim.bold(stackNameToId(stack.stackName) + ":")} New stack`);
29
+ Colors.gap();
30
+ continue;
31
+ }
30
32
  // generate diff
31
33
  const { count, diff } = await Stacks.diff(stack, oldTemplate);
32
34
  spinner.clear();
@@ -58,4 +60,17 @@ export const diff = (program) => program.command("diff", "Compare your app with
58
60
  Colors.line(Colors.success(`✔`), Colors.bold(" Diff:"), changesAcc === 1 ? "1 change found in" : `${changesAcc} changes in`, changedStacks === 1 ? "1 stack" : `${changedStacks} stacks`);
59
61
  }
60
62
  process.exit(0);
63
+ async function getTemplate(stackName) {
64
+ try {
65
+ const response = await cfn.send(new GetTemplateCommand({ StackName: stackName }));
66
+ return JSON.parse(response.TemplateBody);
67
+ }
68
+ catch (e) {
69
+ if (e.name === "ValidationError" &&
70
+ e.message.includes("does not exist")) {
71
+ return;
72
+ }
73
+ throw e;
74
+ }
75
+ }
61
76
  });
@@ -1,5 +1,5 @@
1
1
  import os from "os";
2
- import ciInfo from "ci-info";
2
+ import { getCiInfo } from "../ci-info.js";
3
3
  import { useProject } from "../../project.js";
4
4
  let data;
5
5
  export function getEnvironmentData() {
@@ -7,7 +7,7 @@ export function getEnvironmentData() {
7
7
  return data;
8
8
  }
9
9
  const cpus = os.cpus() || [];
10
- const isSeed = !!process.env.SEED_APP_NAME;
10
+ const ciInfo = getCiInfo();
11
11
  data = {
12
12
  // Software information
13
13
  systemPlatform: os.platform(),
@@ -19,8 +19,8 @@ export function getEnvironmentData() {
19
19
  cpuSpeed: cpus.length ? cpus[0].speed : null,
20
20
  memoryInMb: Math.trunc(os.totalmem() / Math.pow(1024, 2)),
21
21
  // Environment information
22
- isCI: ciInfo.isCI || isSeed,
23
- ciName: (isSeed ? "Seed" : ciInfo.name) || null,
22
+ isCI: ciInfo.isCI,
23
+ ciName: ciInfo.name,
24
24
  sstVersion: useProject().version,
25
25
  };
26
26
  return data;
@@ -48,7 +48,7 @@ export function Functions() {
48
48
  const success = bus.subscribe("function.success", (evt) => {
49
49
  function print(input, diff) {
50
50
  setTimeout(() => {
51
- console.log(Colors.primary(` ➜ `), useFunctions().fromID(input.functionID).handler);
51
+ console.log(Colors.primary(` ➜ `), useFunctions().fromID(input.functionID)?.handler);
52
52
  for (const log of input.logs) {
53
53
  console.log(` ${dim(log)}`);
54
54
  }
@@ -78,7 +78,7 @@ export function Functions() {
78
78
  const error = bus.subscribe("function.error", (evt) => {
79
79
  function print(input, diff) {
80
80
  setTimeout(() => {
81
- console.log(Colors.primary(` ➜ `), useFunctions().fromID(input.functionID).handler);
81
+ console.log(Colors.primary(` ➜ `), useFunctions().fromID(input.functionID)?.handler);
82
82
  for (const log of input.logs) {
83
83
  console.log(` ${dim(log)}`);
84
84
  }
@@ -122,7 +122,7 @@ export function Functions() {
122
122
  " ",
123
123
  React.createElement(Spinner, null),
124
124
  " ",
125
- useFunctions().fromID(evt.functionID).handler),
125
+ useFunctions().fromID(evt.functionID)?.handler),
126
126
  evt.logs.map((log, index) => (React.createElement(Text, { dimColor: true, key: index },
127
127
  " ",
128
128
  log))),
@@ -10,7 +10,7 @@ import { Permissions } from "./util/permission.js";
10
10
  import { Table as CDKTable } from "aws-cdk-lib/aws-dynamodb";
11
11
  import { IServerlessCluster } from "aws-cdk-lib/aws-rds";
12
12
  import { ISecret } from "aws-cdk-lib/aws-secretsmanager";
13
- import { AwsIamConfig, BaseDataSource, CfnDomainName, GraphqlApi, GraphqlApiProps, IGraphqlApi, Resolver, ResolverProps } from "aws-cdk-lib/aws-appsync";
13
+ import { AwsIamConfig, BaseDataSource, GraphqlApi, GraphqlApiProps, IGraphqlApi, Resolver, ResolverProps } from "aws-cdk-lib/aws-appsync";
14
14
  import { ICertificate } from "aws-cdk-lib/aws-certificatemanager";
15
15
  import { IDomain } from "aws-cdk-lib/aws-opensearchservice";
16
16
  export interface AppSyncApiDomainProps extends appSyncApiDomain.CustomDomainProps {
@@ -403,7 +403,6 @@ export declare class AppSyncApi extends Construct implements SSTConstruct {
403
403
  };
404
404
  private readonly props;
405
405
  private _customDomainUrl?;
406
- _cfnDomainName?: CfnDomainName;
407
406
  private readonly functionsByDsKey;
408
407
  private readonly dataSourcesByDsKey;
409
408
  private readonly dsKeysByResKey;
@@ -48,7 +48,6 @@ export class AppSyncApi extends Construct {
48
48
  cdk;
49
49
  props;
50
50
  _customDomainUrl;
51
- _cfnDomainName;
52
51
  functionsByDsKey = {};
53
52
  dataSourcesByDsKey = {};
54
53
  dsKeysByResKey = {};
@@ -308,24 +307,15 @@ export class AppSyncApi extends Construct {
308
307
  name: app.logicalPrefixedName(id),
309
308
  xrayEnabled: true,
310
309
  schema: mainSchema,
311
- domainName: domainData,
310
+ domainName: domainData && {
311
+ certificate: domainData.certificate,
312
+ domainName: domainData.domainName,
313
+ },
312
314
  ...graphqlApiProps,
313
315
  });
314
316
  this.cdk.certificate = domainData?.certificate;
315
- // note: As of CDK 2.20.0, the "AWS::AppSync::DomainNameApiAssociation" resource
316
- // is not dependent on the "AWS::AppSync::DomainName" resource. This leads
317
- // CloudFormation deploy error if DomainNameApiAssociation is created before
318
- // DomainName is created.
319
- // https://github.com/aws/aws-cdk/issues/18395#issuecomment-1099455502
320
- // To workaround this issue, we need to add a dependency manually.
321
317
  if (domainData) {
322
- this._cfnDomainName = this.cdk.graphqlApi.node.children.find((child) => child.cfnResourceType ===
323
- "AWS::AppSync::DomainName");
324
- const cfnDomainNameApiAssociation = this.cdk.graphqlApi.node.children.find((child) => child.cfnResourceType ===
325
- "AWS::AppSync::DomainNameApiAssociation");
326
- if (this._cfnDomainName && cfnDomainNameApiAssociation) {
327
- cfnDomainNameApiAssociation.node.addDependency(this._cfnDomainName);
328
- }
318
+ appSyncApiDomain.cleanup(this, domainData);
329
319
  }
330
320
  }
331
321
  }
@@ -590,7 +590,7 @@ export declare class Function extends CDKFunction implements SSTConstruct {
590
590
  static mergeProps(baseProps?: FunctionProps, props?: FunctionProps): FunctionProps;
591
591
  }
592
592
  export declare const useFunctions: () => {
593
- fromID(id: string): FunctionProps;
593
+ fromID(id: string): FunctionProps | undefined;
594
594
  add(name: string, props: FunctionProps): void;
595
595
  readonly all: Record<string, FunctionProps>;
596
596
  };
@@ -171,7 +171,9 @@ export class Function extends CDKFunction {
171
171
  new PolicyStatement({
172
172
  actions: ["s3:*"],
173
173
  effect: Effect.ALLOW,
174
- resources: [`arn:aws:s3:::${bootstrap.bucket}`],
174
+ resources: [
175
+ `arn:${Stack.of(this).partition}:s3:::${bootstrap.bucket}`,
176
+ ],
175
177
  }),
176
178
  ]);
177
179
  });
@@ -499,7 +501,10 @@ export const useFunctions = createAppContext(() => {
499
501
  const functions = {};
500
502
  return {
501
503
  fromID(id) {
502
- return functions[id];
504
+ const result = functions[id];
505
+ if (!result)
506
+ return;
507
+ return result;
503
508
  },
504
509
  add(name, props) {
505
510
  functions[name] = props;
@@ -16,7 +16,7 @@ export function getOrCreateBucket(scope) {
16
16
  }
17
17
  // Create provider
18
18
  const provider = new lambda.Function(stack, providerId, {
19
- code: lambda.Code.fromAsset(path.join(__dirname, "../../../support/edge-function")),
19
+ code: lambda.Code.fromAsset(path.join(__dirname, "../../support/edge-function")),
20
20
  handler: "s3-bucket.handler",
21
21
  runtime: lambda.Runtime.NODEJS_16_X,
22
22
  timeout: cdk.Duration.minutes(15),
@@ -48,7 +48,7 @@ export function createFunction(scope, name, role, bucketName, functionParams) {
48
48
  // Create provider if not already created
49
49
  if (!provider) {
50
50
  provider = new lambda.Function(stack, providerId, {
51
- code: lambda.Code.fromAsset(path.join(__dirname, "../../../support/edge-function")),
51
+ code: lambda.Code.fromAsset(path.join(__dirname, "../../support/edge-function")),
52
52
  handler: "edge-lambda.handler",
53
53
  runtime: lambda.Runtime.NODEJS_16_X,
54
54
  timeout: cdk.Duration.minutes(15),
@@ -86,7 +86,7 @@ export function createVersion(scope, name, functionArn) {
86
86
  // Create provider if not already created
87
87
  if (!provider) {
88
88
  provider = new lambda.Function(stack, providerId, {
89
- code: lambda.Code.fromAsset(path.join(__dirname, "../../../support/edge-function")),
89
+ code: lambda.Code.fromAsset(path.join(__dirname, "../../support/edge-function")),
90
90
  handler: "edge-lambda-version.handler",
91
91
  runtime: lambda.Runtime.NODEJS_16_X,
92
92
  timeout: cdk.Duration.minutes(15),
@@ -1,5 +1,4 @@
1
1
  import * as route53 from "aws-cdk-lib/aws-route53";
2
- import * as appsync from "aws-cdk-lib/aws-appsync";
3
2
  import * as acm from "aws-cdk-lib/aws-certificatemanager";
4
3
  import { AppSyncApi } from "../AppSyncApi.js";
5
4
  export interface CustomDomainProps {
@@ -11,6 +10,11 @@ export interface CustomDomainProps {
11
10
  * The hosted zone in Route 53 that contains the domain. By default, SST will look for a hosted zone by stripping out the first part of the domainName that's passed in. So, if your domainName is api.domain.com. SST will default the hostedZone to domain.com.
12
11
  */
13
12
  hostedZone?: string;
13
+ /**
14
+ * DNS record type for the Route 53 record associated with the custom domain. Default is CNAME.
15
+ * @default CNAME
16
+ */
17
+ recordType?: "CNAME" | "A_AAAA";
14
18
  /**
15
19
  * Set this option if the domain is not hosted on Amazon Route 53.
16
20
  */
@@ -26,4 +30,12 @@ export interface CustomDomainProps {
26
30
  certificate?: acm.ICertificate;
27
31
  };
28
32
  }
29
- export declare function buildCustomDomainData(scope: AppSyncApi, customDomain: string | CustomDomainProps | undefined): appsync.DomainOptions | undefined;
33
+ interface CustomDomainData {
34
+ certificate: acm.ICertificate;
35
+ domainName: string;
36
+ hostedZone?: route53.IHostedZone;
37
+ recordType?: CustomDomainProps["recordType"];
38
+ }
39
+ export declare function buildCustomDomainData(scope: AppSyncApi, customDomain: string | CustomDomainProps | undefined): CustomDomainData | undefined;
40
+ export declare function cleanup(scope: AppSyncApi, domainData: CustomDomainData): void;
41
+ export {};
@@ -1,5 +1,6 @@
1
- import { Token, Lazy } from "aws-cdk-lib";
1
+ import { Token } from "aws-cdk-lib";
2
2
  import * as route53 from "aws-cdk-lib/aws-route53";
3
+ import * as route53Targets from "aws-cdk-lib/aws-route53-targets";
3
4
  import * as acm from "aws-cdk-lib/aws-certificatemanager";
4
5
  export function buildCustomDomainData(scope, customDomain) {
5
6
  if (customDomain === undefined) {
@@ -18,6 +19,14 @@ export function buildCustomDomainData(scope, customDomain) {
18
19
  // customDomain.domainName not exists
19
20
  throw new Error(`Missing "domainName" in sst.AppSyncApi's customDomain setting`);
20
21
  }
22
+ export function cleanup(scope, domainData) {
23
+ const cfnDomainName = getCfnDomainName(scope);
24
+ const cfnDomainNameApiAssociation = getCfnDomainNameApiAssociation(scope);
25
+ if (domainData.hostedZone) {
26
+ createRecords(scope, domainData.domainName, domainData.hostedZone, domainData.recordType, cfnDomainName);
27
+ }
28
+ fixDomainResourceDependencies(cfnDomainName, cfnDomainNameApiAssociation);
29
+ }
21
30
  function buildDataForStringInput(scope, customDomain) {
22
31
  // validate: customDomain is a TOKEN string
23
32
  // ie. imported SSM value: ssm.StringParameter.valueForStringParameter()
@@ -29,10 +38,10 @@ function buildDataForStringInput(scope, customDomain) {
29
38
  const hostedZoneDomain = domainName.split(".").slice(1).join(".");
30
39
  const hostedZone = lookupHostedZone(scope, hostedZoneDomain);
31
40
  const certificate = createCertificate(scope, domainName, hostedZone);
32
- createRecord(scope, hostedZone, domainName);
33
41
  return {
34
42
  certificate,
35
43
  domainName,
44
+ hostedZone,
36
45
  };
37
46
  }
38
47
  function buildDataForInternalDomainInput(scope, customDomain) {
@@ -69,10 +78,11 @@ function buildDataForInternalDomainInput(scope, customDomain) {
69
78
  const certificate = customDomain.cdk?.certificate
70
79
  ? customDomain.cdk.certificate
71
80
  : createCertificate(scope, domainName, hostedZone);
72
- createRecord(scope, hostedZone, domainName);
73
81
  return {
74
82
  certificate,
75
83
  domainName,
84
+ hostedZone,
85
+ recordType: customDomain.recordType,
76
86
  };
77
87
  }
78
88
  function buildDataForExternalDomainInput(scope, customDomain) {
@@ -103,25 +113,60 @@ function createCertificate(scope, domainName, hostedZone) {
103
113
  validation: acm.CertificateValidation.fromDns(hostedZone),
104
114
  });
105
115
  }
106
- function createRecord(scope, hostedZone, domainName) {
116
+ function createRecords(scope, domainName, hostedZone, recordType, cfnDomainName) {
107
117
  // create DNS record
108
- const record = new route53.CnameRecord(scope, "CnameRecord", {
118
+ const aRecordProps = {
109
119
  recordName: domainName,
110
120
  zone: hostedZone,
111
- domainName: Lazy.string({
112
- produce() {
113
- return scope._cfnDomainName.attrAppSyncDomainName;
121
+ target: route53.RecordTarget.fromAlias({
122
+ bind() {
123
+ return {
124
+ hostedZoneId: route53Targets.CloudFrontTarget.CLOUDFRONT_ZONE_ID,
125
+ dnsName: cfnDomainName.attrAppSyncDomainName,
126
+ };
114
127
  },
115
128
  }),
116
- });
129
+ };
130
+ const records = (recordType || "CNAME") === "CNAME"
131
+ ? [
132
+ new route53.CnameRecord(scope, "CnameRecord", {
133
+ recordName: domainName,
134
+ zone: hostedZone,
135
+ domainName: cfnDomainName.attrAppSyncDomainName,
136
+ }),
137
+ ]
138
+ : [
139
+ new route53.ARecord(scope, "AliasRecord", aRecordProps),
140
+ new route53.AaaaRecord(scope, "AliasRecordAAAA", aRecordProps),
141
+ ];
117
142
  // note: If domainName is a TOKEN string ie. ${TOKEN..}, the route53.ARecord
118
143
  // construct will append ".${hostedZoneName}" to the end of the domain.
119
144
  // This is because the construct tries to check if the record name
120
145
  // ends with the domain name. If not, it will append the domain name.
121
146
  // So, we need remove this behavior.
122
147
  if (Token.isUnresolved(domainName)) {
123
- const cfnRecord = record.node.defaultChild;
124
- cfnRecord.name = domainName;
148
+ records.forEach((record) => {
149
+ const cfnRecord = record.node.defaultChild;
150
+ cfnRecord.name = domainName;
151
+ });
152
+ }
153
+ }
154
+ function getCfnDomainName(scope) {
155
+ return scope.cdk.graphqlApi.node.children.find((child) => child.cfnResourceType === "AWS::AppSync::DomainName");
156
+ }
157
+ function getCfnDomainNameApiAssociation(scope) {
158
+ return scope.cdk.graphqlApi.node.children.find((child) => child.cfnResourceType ===
159
+ "AWS::AppSync::DomainNameApiAssociation");
160
+ }
161
+ function fixDomainResourceDependencies(cfnDomainName, cfnDomainNameApiAssociation) {
162
+ // note: As of CDK 2.20.0, the "AWS::AppSync::DomainNameApiAssociation" resource
163
+ // is not dependent on the "AWS::AppSync::DomainName" resource. This leads
164
+ // CloudFormation deploy error if DomainNameApiAssociation is created before
165
+ // DomainName is created.
166
+ // https://github.com/aws/aws-cdk/issues/18395#issuecomment-1099455502
167
+ // To workaround this issue, we need to add a dependency manually.
168
+ if (cfnDomainName && cfnDomainNameApiAssociation) {
169
+ cfnDomainNameApiAssociation.node.addDependency(cfnDomainName);
125
170
  }
126
171
  }
127
172
  function assertDomainNameIsLowerCase(domainName) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "sideEffects": false,
3
3
  "name": "sst",
4
- "version": "2.7.1",
4
+ "version": "2.7.2",
5
5
  "bin": {
6
6
  "sst": "cli/sst.js"
7
7
  },
@@ -25,6 +25,11 @@ export const useRuntimeHandlers = Context.memo(() => {
25
25
  async build(functionID, mode) {
26
26
  async function task() {
27
27
  const func = useFunctions().fromID(functionID);
28
+ if (!func)
29
+ return {
30
+ type: "error",
31
+ errors: [`Function with ID "${functionID}" not found`],
32
+ };
28
33
  const handler = result.for(func.runtime);
29
34
  const out = path.join(project.paths.artifacts, functionID);
30
35
  await fs.rm(out, { recursive: true, force: true });
@@ -100,6 +105,8 @@ export const useFunctionBuilder = Context.memo(() => {
100
105
  },
101
106
  build: async (functionID) => {
102
107
  const result = await handlers.build(functionID, "start");
108
+ if (!result)
109
+ return;
103
110
  if (result.type === "error")
104
111
  return;
105
112
  artifacts.set(functionID, result);
@@ -13,6 +13,8 @@ export const useRuntimeWorkers = Context.memo(async () => {
13
13
  for (const [_, worker] of workers) {
14
14
  if (worker.functionID === evt.properties.functionID) {
15
15
  const props = useFunctions().fromID(worker.functionID);
16
+ if (!props)
17
+ return;
16
18
  const handler = handlers.for(props.runtime);
17
19
  await handler?.stopWorker(worker.workerID);
18
20
  bus.publish("worker.stopped", worker);
@@ -30,6 +32,8 @@ export const useRuntimeWorkers = Context.memo(async () => {
30
32
  if (worker)
31
33
  return;
32
34
  const props = useFunctions().fromID(evt.properties.functionID);
35
+ if (!props)
36
+ return;
33
37
  const handler = handlers.for(props.runtime);
34
38
  if (!handler)
35
39
  return;