+
count is {count}
@@ -266,147 +411,182 @@ function App() {
export default App;
```
-4. Once you save the code, you can examine both the webpage and the Simulator to see how the counter gets incremented.
-## Step 4 - Create a broadcasting service using `@winglibs/websockets`
+
+
+## Step 4 - Synchronize browsers using `@winglibs/websockets`
-In the current implementation when we open two browser side by side, we only
-see the counter latest value upon refresh. In this step we will be deploying a broadcasting service
-that utilize a websocket server on the backend, and connecting to that websocket from the clients.
+In the current implementation, if we open two browser side-by-side, we only see the counter latest
+value upon refresh.
-When the counter is incremented, the broadcaster service will notify all clients that they need to fetch a
-new value from our API.
+In this step we will create a broadcasting service which deploys a WebSocket server on the backend.
+Clients then connect to this WebSocket to receive real-time notifications when the counter is
+updated.
+
+When the counter is incremented, the broadcaster service will notify all clients that they need to
+fetch a new value from our API.
### Create a Broadcaster class
-The Broadcaster class contains two public API endpoints:
-- a public websocket URL that will be sent to clients
+The `Broadcaster` class contains two public API endpoints:
+
+- a static public WebSocket URL that will be sent to clients through `publicEnv`.
- an `inflight` broadcast message, that sends a message to all connected clients
-1. We will be using `@winglibs/websockets`, so lets first install it
-```sh
-npm i -s @winglibs/websockets
-```
-2. Let's create a new file `backend/broadcaster.w`, and implement it as follows:
-```ts
-bring cloud;
-bring websockets;
-
-pub class Broadcaster {
- pub wsUrl: str;
- wb: websockets.WebSocket;
- db: cloud.Bucket;
- new() {
- this.wb = new websockets.WebSocket(name: "MyWebSocket") as "my-websocket";
- this.wsUrl = this.wb.url;
- this.db = new cloud.Bucket();
-
- this.wb.onConnect(inflight(id: str): void => {
- this.db.put(id, "");
- });
+1. First, let's install the `@winglibs/websockets` library:
+
+ ```sh
+ cd ~/shared-counter/backend
+ npm i @winglibs/websockets
+ ```
+
+2. Create a new file `backend/broadcaster.w`, with the following implementation:
+
+ ```ts
+ bring cloud;
+ bring websockets;
+
+ pub class Broadcaster {
+ pub url: str;
+ server: websockets.WebSocket;
+ clients: cloud.Bucket;
+
+ new() {
+ this.server = new websockets.WebSocket(name: "counter_updates");
+ this.url = this.server.url;
+ this.clients = new cloud.Bucket();
+
+ // upon connection, add the client to the list
+ this.server.onConnect(inflight(id: str): void => {
+ this.clients.put(id, "");
+ });
+
+ // upon disconnect, remove the client from the list
+ this.server.onDisconnect(inflight(id: str): void => {
+ this.clients.delete(id);
+ });
+ }
+
+ // send a message to all clients
+ pub inflight broadcast(message: str) {
+ for id in this.clients.list() {
+ this.server.sendMessage(id, message);
+ }
+ }
+ }
+ ```
+
+3. In `backend/main.w`, lets bring and instantiate our broadcaster service:
- this.wb.onDisconnect(inflight(id: str): void => {
- this.db.delete(id);
+ ```ts
+ bring "./broadcaster.w" as b;
+
+ let broadcaster = new b.Broadcaster();
+ ```
+
+4. Send the WebSocket URL to the client:
+
+ ```ts
+ new vite.Vite(
+ root: "../frontend",
+ publicEnv: {
+ TITLE: "Wing + Vite + React",
+ WS_URL: broadcaster.url, // <-- add this
+ API_URL: api.url,
+ }
+ );
+ ```
+
+5. Now, every time the counter is increment, let's send a broadcast `"refresh"` message to all our clients. Add this to the `POST /counter` handler:
+
+ ```ts
+ api.post("/counter", inflight () => {
+ let oldValue = counter.inc();
+ broadcaster.broadcast("refresh");
+
+ return {
+ body: "{oldValue + 1}"
+ };
});
+ ```
- }
- pub inflight broadcast(message: str) {
- for id in this.db.list() {
- this.wb.sendMessage(id, message);
- }
- }
-}
-```
-3. In our `backend/main.w` file lets instantiate the broadcasting service:
-```ts
-bring "./broadcaster.w" as b;
+---
-let broadcaster = new b.Broadcaster();
-```
-4. Send the websocket url to the client
-```ts
-new vite.Vite(
- root: "../frontend",
- publicEnv: {
- title: "Wing + Vite + React",
- API_URL: api.url,
- WS_URL: broadcaster.wsUrl // <-- This is new
- }
-);
-```
-5. Also, lets send a broadcast "refresh" message every time we increment the counter,
-in `backend/main.w` `post` endpoint:
-```ts
-api.post("/counter", inflight () => {
- let oldValue = counter.inc();
- broadcaster.broadcast("refresh");
- return {
- status: 200,
- body: "{oldValue + 1}"
- };
-});
-```
+
+main.w
-6. For convenience, here is the entire `backend/main.w` file:
```ts
bring vite;
bring cloud;
bring "./broadcaster.w" as b;
let broadcaster = new b.Broadcaster();
-let api = new cloud.Api(cors: true);
-new vite.Vite(
- root: "../frontend",
- publicEnv: {
- title: "Wing + Vite + React",
- API_URL: api.url,
- WS_URL: broadcaster.wsUrl
- }
-);
+let api = new cloud.Api(cors: true);
let counter = new cloud.Counter();
+
api.get("/counter", inflight () => {
return {
- status: 200,
body: "{counter.peek()}"
};
});
api.post("/counter", inflight () => {
- let oldValue = counter.inc();
+ let prev = counter.inc();
broadcaster.broadcast("refresh");
return {
- status: 200,
- body: "{oldValue + 1}"
+ body: "{prev + 1}"
};
});
+
+new vite.Vite(
+ root: "../frontend",
+ publicEnv: {
+ TITLE: "Wing + Vite + React",
+ WS_URL: broadcaster.url,
+ API_URL: api.url,
+ }
+);
```
+
### Listen to ws message and trigger data refresh
-On the client side we are going to use `react-use-websocket` and listen to any
-event from the broadcaster, once an event is received we will read the counter value from
-the API.
+Let's move to the client.
+
+On the client side we are going to use `react-use-websocket` and listen to any event from the
+broadcaster, once an event is received we will read the counter value from the API.
1. Start by installing `react-use-websocket` on the `frontend/`:
-```sh
-cd ~/shared-counter/frontend
-npm i -s react-use-websocket
-```
+ ```sh
+ cd ~/shared-counter/frontend
+ npm i react-use-websocket
+ ```
+
2. Lets import and use it inside `frontend/App.tsx`:
-```ts
-import useWebSocket from 'react-use-websocket';
-```
-3. And use it inside the `App()` function body:
-```ts
-useWebSocket(window.wing.env.WS_URL, {
- onMessage: () => {
- getCount();
- }
-});
-```
-4. For convenience, here is the entire `App.tsx` file:
+
+ ```ts
+ import useWebSocket from 'react-use-websocket';
+ ```
+
+3. And use it inside the `App()` function body (after the definition of `updateCount()`):
+
+ ```ts
+ useWebSocket(window.wing.env.WS_URL, {
+ onMessage: () => {
+ updateCount();
+ }
+ });
+ ```
+
+5. Play around by opening multiple tabs of the website; they should automatically update when the
+ counter increments.
+
+---
+
+
+App.tsx
```ts
import { useState, useEffect } from 'react'
@@ -419,24 +599,29 @@ import useWebSocket from 'react-use-websocket';
function App() {
const API_URL = window.wing.env.API_URL;
const [count, setCount] = useState("NA")
+
const incrementCount = async () => {
const response = await fetch(`${API_URL}/counter`, {
method: "POST"
});
setCount(await response.text());
};
- const getCount = async () => {
+
+ const updateCount = async () => {
const response = await fetch(`${API_URL}/counter`);
setCount(await response.text());
};
+
useWebSocket(window.wing.env.WS_URL, {
onMessage: () => {
- getCount();
+ updateCount();
}
});
+
useEffect(() => {
- getCount();
+ updateCount();
}, []);
+
return (
<>
@@ -447,9 +632,9 @@ function App() {
- {window.wing.env.title}
+ {window.wing.env.TITLE}
-
+
count is {count}
@@ -465,8 +650,8 @@ function App() {
export default App;
```
-5. Play around by opening multiple tabs of the website; they should automatically update when the counter increments.
+
## Step 5 - Deploy on AWS
@@ -479,27 +664,24 @@ Once deployed, the above code translates into the following (simplified) AWS arc
In order to deploy to AWS, you will need:
* [Terraform](https://terraform.io/downloads)
-* AWS CLI with configured credentials. See
-[here](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html)
-for more information.
+* [AWS CLI](https://docs.aws.amazon.com/cli) with configured credentials. See
+ [here](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html) for more
+ information.
1. Compile to Terraform/AWS
-We will use the `tf-aws` platform to tell the compiler to bind all of our resources
-to the default set of AWS resources and use Terraform as the provisioning engine.
+ We will use the `tf-aws` platform to tell the compiler to bind all of our resources
+ to the default set of AWS resources and use Terraform as the provisioning engine.
-```sh
-cd ~/shared-counter/backend
-wing compile --platform tf-aws main.w
-```
+ ```sh
+ cd ~/shared-counter/backend
+ wing compile --platform tf-aws main.w
+ ```
2. Run Terraform Init and Apply
-```sh
-cd ./target/main.tfaws
-terraform init
-terraform apply # this takes some time
-```
-
-
-
+ ```sh
+ cd ./target/main.tfaws
+ terraform init
+ terraform apply # this takes some time
+ ```
diff --git a/examples/tests/invalid/stringify.test.w b/examples/tests/invalid/stringify.test.w
new file mode 100644
index 00000000000..123cd1b0621
--- /dev/null
+++ b/examples/tests/invalid/stringify.test.w
@@ -0,0 +1,8 @@
+class B {}
+let b = new B();
+log("hello {b}");
+// ^ Error: expected type to be stringable
+
+let x: str? = nil;
+log("{x}");
+// ^ Error: expected type to be stringable
diff --git a/examples/tests/invalid/void_in_expression_position.test.w b/examples/tests/invalid/void_in_expression_position.test.w
index e2251178a40..7cf3e6a12f0 100644
--- a/examples/tests/invalid/void_in_expression_position.test.w
+++ b/examples/tests/invalid/void_in_expression_position.test.w
@@ -2,7 +2,7 @@ log("hey").get("x");
// ^^^^^^^^^ Expression must be a class to access property "get", instead found type "void"
let x = "my name is {log("mister cloud")}";
-// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Expected type to be one of "str,num", but got "void" instead
+// ^^^^^^^^^^^^^^^^^^^ Expected type to be stringable, but got "void" instead
let y = 5 + log("hello");
// ^^^^^^^^^^^^^^ Expected type to be "num", but got "void" instead
diff --git a/examples/tests/sdk_tests/function/concurrency.test.w b/examples/tests/sdk_tests/function/concurrency.test.w
new file mode 100644
index 00000000000..699c22e028c
--- /dev/null
+++ b/examples/tests/sdk_tests/function/concurrency.test.w
@@ -0,0 +1,44 @@
+bring cloud;
+bring util;
+
+// TODO: support concurrency on AWS
+
+if util.env("WING_TARGET") == "sim" {
+ let c = new cloud.Counter();
+
+ let f1 = new cloud.Function(inflight () => {
+ c.inc();
+ util.sleep(5s);
+ }, concurrency: 1) as "concurrency fn";
+
+ test "f1 concurrency limit reached" {
+ f1.invokeAsync();
+ try {
+ f1.invoke();
+ } catch e {
+ assert(e.contains("Too many requests, the function has reached its concurrency limit"));
+ return;
+ }
+
+ log("No error thrown");
+ assert(false);
+ }
+
+ let q = new cloud.Queue();
+
+ q.setConsumer(inflight (message: str) => {
+ util.sleep(1s);
+ c.inc();
+ }, concurrency: 1, batchSize: 1);
+
+ test "queue applies backpressure to functions with limited concurrency" {
+ q.push("m1");
+ q.push("m2");
+ q.push("m3");
+
+ util.sleep(5s);
+
+ log("c: {c.peek()}");
+ assert(c.peek() == 3);
+ }
+}
diff --git a/examples/tests/sdk_tests/function/invoke.test.w b/examples/tests/sdk_tests/function/invoke.test.w
index 482b0a1f095..35196871d62 100644
--- a/examples/tests/sdk_tests/function/invoke.test.w
+++ b/examples/tests/sdk_tests/function/invoke.test.w
@@ -10,7 +10,7 @@ let f = new cloud.Function(inflight (input): str => {
let target = util.tryEnv("WING_TARGET");
assert(target?); // make sure WING_TARGET is defined in all environments
- return "{input}-response";
+ return "{input ?? "nil"}-response";
});
test "invoke" {
@@ -34,4 +34,4 @@ test "invoke without inputs and outputs" {
let response = f3.invoke();
expect.equal(response, nil);
-}
\ No newline at end of file
+}
diff --git a/examples/tests/valid/bring_local_dir.test.w b/examples/tests/valid/bring_local_dir.test.w
index a4819cbadcb..8d53a31371e 100644
--- a/examples/tests/valid/bring_local_dir.test.w
+++ b/examples/tests/valid/bring_local_dir.test.w
@@ -1,7 +1,7 @@
bring "./subdir2/inner/widget.w" as w;
bring "./subdir2" as subdir;
-let widget1 = new w.Widget();
+let widget1 = new w.Widget() as "widget1";
assert(widget1.compute() == 42);
// from subdir/file1.w
@@ -13,7 +13,7 @@ let bar = new subdir.Bar();
assert(bar.bar() == "bar");
// from subdir/inner/widget.w
-let widget2 = new subdir.inner.Widget();
+let widget2 = new subdir.inner.Widget() as "widget2";
assert(widget2.compute() == 42);
assert(foo.checkWidget(widget2) == 1379);
diff --git a/examples/tests/valid/doubler.test.w b/examples/tests/valid/doubler.test.w
index 454a09dcfb2..04f12c1d9d0 100644
--- a/examples/tests/valid/doubler.test.w
+++ b/examples/tests/valid/doubler.test.w
@@ -15,7 +15,7 @@ class Doubler {
}
let fn = new Doubler(inflight (m: str?): str => {
- return "Hello {m}!";
+ return "Hello {m ?? "nil"}!";
});
// ----------
diff --git a/examples/tests/valid/enums.test.w b/examples/tests/valid/enums.test.w
index f006c94899f..cb8ccaf5faa 100644
--- a/examples/tests/valid/enums.test.w
+++ b/examples/tests/valid/enums.test.w
@@ -20,3 +20,14 @@ test "inflight" {
assert(one == SomeEnum.ONE);
assert(two == SomeEnum.TWO);
}
+
+// values stringify into their own names
+assert("{SomeEnum.ONE}" == "ONE");
+assert("{SomeEnum.TWO}" == "TWO");
+assert("{SomeEnum.THREE}" == "THREE");
+
+test "toStr inflight" {
+ assert("{SomeEnum.ONE}" == "ONE");
+ assert("{SomeEnum.TWO}" == "TWO");
+ assert("{SomeEnum.THREE}" == "THREE");
+}
diff --git a/examples/tests/valid/inflight_handler_singleton.test.w b/examples/tests/valid/inflight_handler_singleton.test.w
index d5d526a2c28..c4cf053c5d8 100644
--- a/examples/tests/valid/inflight_handler_singleton.test.w
+++ b/examples/tests/valid/inflight_handler_singleton.test.w
@@ -12,6 +12,10 @@ class Foo {
this.n += 1;
return this.n;
}
+
+ pub inflight get(): num {
+ return this.n;
+ }
}
let foo = new Foo();
@@ -34,9 +38,27 @@ test "single instance of Foo" {
let z = fn2.invoke("");
expect.equal(x, "100");
+
+ // the simulator intentionally reuses the sandbox across invocations
+ // but we can't trust that this will always happen on the cloud
+ if sim {
+ expect.equal(y, "101");
+ log("client has been reused");
+ }
+
expect.equal(z, "100-fn2"); // fn2 should have a separate instance
+}
+
+// a function that takes at least three seconds to run
+let fn3 = new cloud.Function(inflight () => {
+ let n = foo.inc();
+ util.sleep(3s);
+ assert(n == foo.get());
+}) as "fn3";
- // y could be 100 or 101 depending on whether the execution environment
- // was reused or not between the two calls.
- assert(y == "100" || y == "101");
+test "Foo state is not shared between function invocations" {
+ // start two invocations of fn, staggering them by 1 second
+ fn3.invokeAsync("");
+ util.sleep(1s);
+ fn3.invoke("");
}
diff --git a/examples/tests/valid/redis.test.w b/examples/tests/valid/redis.test.w
index 27defe8c8e4..617bc9a1325 100644
--- a/examples/tests/valid/redis.test.w
+++ b/examples/tests/valid/redis.test.w
@@ -30,5 +30,5 @@ test "testing Redis" {
return r.get("hello") != nil;
});
- assert("world!" == "{r.get("hello")}");
+ assert("world!" == "{r.get("hello") ?? "nil"}");
}
diff --git a/examples/wing-fixture/turbo.json b/examples/wing-fixture/turbo.json
index 06b4cea5490..c2dcd75a401 100644
--- a/examples/wing-fixture/turbo.json
+++ b/examples/wing-fixture/turbo.json
@@ -4,7 +4,8 @@
"pipeline": {
"compile": {
"dependsOn": ["^compile"],
- "inputs": ["**/*.w", "**/*.js", "**/*.ts"]
+ "inputs": ["**/*.w", "**/*.js", "**/*.ts"],
+ "outputs": ["target/wing-fixture.wsim/**"]
},
"topo": {}
}
diff --git a/libs/awscdk/src/api.ts b/libs/awscdk/src/api.ts
index a188436ea82..927e190220a 100644
--- a/libs/awscdk/src/api.ts
+++ b/libs/awscdk/src/api.ts
@@ -10,18 +10,18 @@ import {
import { CfnPermission } from "aws-cdk-lib/aws-lambda";
import { Construct } from "constructs";
import { App } from "./app";
-import { Function } from "./function";
import { cloud, core, std } from "@winglang/sdk";
import { convertBetweenHandlers } from "@winglang/sdk/lib/shared/convert";
import { IAwsApi, STAGE_NAME } from "@winglang/sdk/lib/shared-aws/api";
import { API_DEFAULT_RESPONSE } from "@winglang/sdk/lib/shared-aws/api.default";
+import { isAwsCdkFunction } from "./function";
/**
* AWS Implementation of `cloud.Api`.
*/
export class Api extends cloud.Api implements IAwsApi {
private readonly api: WingRestApi;
- private readonly handlers: Record = {};
+ private readonly handlers: Record = {};
private readonly endpoint: cloud.Endpoint;
constructor(scope: Construct, id: string, props: cloud.ApiProps = {}) {
@@ -187,12 +187,8 @@ export class Api extends cloud.Api implements IAwsApi {
inflight: cloud.IApiEndpointHandler,
method: string,
path: string
- ): Function {
- let fn = this.addInflightHandler(inflight, method, path);
- if (!(fn instanceof Function)) {
- throw new Error("Api only supports creating tfaws.Function right now");
- }
- return fn;
+ ): cloud.Function {
+ return this.addInflightHandler(inflight, method, path);
}
/**
@@ -205,7 +201,7 @@ export class Api extends cloud.Api implements IAwsApi {
inflight: cloud.IApiEndpointHandler,
method: string,
path: string
- ): Function {
+ ): cloud.Function {
let handler = this.handlers[inflight._id];
if (!handler) {
const newInflight = convertBetweenHandlers(
@@ -218,7 +214,7 @@ export class Api extends cloud.Api implements IAwsApi {
}
);
const prefix = `${method.toLowerCase()}${path.replace(/\//g, "_")}_}`;
- handler = new Function(
+ handler = new cloud.Function(
this,
App.of(this).makeId(this, prefix),
newInflight
@@ -231,12 +227,7 @@ export class Api extends cloud.Api implements IAwsApi {
/** @internal */
public onLift(host: std.IInflightHost, ops: string[]): void {
- if (!(host instanceof Function)) {
- throw new Error("apis can only be bound by awscdk.Function for now");
- }
-
host.addEnvironment(this.urlEnvName(), this.url);
-
super.onLift(host, ops);
}
@@ -338,7 +329,7 @@ class WingRestApi extends Construct {
* @param handler Lambda function to handle the endpoint
* @returns OpenApi spec extension for the endpoint
*/
- public addEndpoint(path: string, method: string, handler: Function) {
+ public addEndpoint(path: string, method: string, handler: cloud.Function) {
const endpointExtension = this.createApiSpecExtension(handler);
this.addHandlerPermissions(path, method, handler);
return endpointExtension;
@@ -349,10 +340,14 @@ class WingRestApi extends Construct {
* @param handler Lambda function to handle the endpoint
* @returns OpenApi extension object for the endpoint and handler
*/
- private createApiSpecExtension(handler: Function) {
+ private createApiSpecExtension(handler: cloud.Function) {
+ if (!isAwsCdkFunction(handler)) {
+ throw new Error("Expected 'handler' to implement IAwsCdkFunction");
+ }
+
const extension = {
"x-amazon-apigateway-integration": {
- uri: `arn:aws:apigateway:${this.region}:lambda:path/2015-03-31/functions/${handler.functionArn}/invocations`,
+ uri: `arn:aws:apigateway:${this.region}:lambda:path/2015-03-31/functions/${handler.awscdkFunction.functionArn}/invocations`,
type: "aws_proxy",
httpMethod: "POST",
responses: {
@@ -377,13 +372,18 @@ class WingRestApi extends Construct {
private addHandlerPermissions = (
path: string,
method: string,
- handler: Function
+ handler: cloud.Function
) => {
+ if (!isAwsCdkFunction(handler)) {
+ throw new Error("Expected 'handler' to implement IAwsCdkFunction");
+ }
+
const pathHash = createHash("sha1").update(path).digest("hex").slice(-8);
const permissionId = `${method}-${pathHash}`;
+
new CfnPermission(this, `permission-${permissionId}`, {
action: "lambda:InvokeFunction",
- functionName: handler.functionName,
+ functionName: handler.awscdkFunction.functionName,
principal: "apigateway.amazonaws.com",
sourceArn: this.api.arnForExecuteApi(method, Api._toOpenApiPath(path)),
});
diff --git a/libs/awscdk/src/app.ts b/libs/awscdk/src/app.ts
index 5f914fdf20d..ccf9addc6be 100644
--- a/libs/awscdk/src/app.ts
+++ b/libs/awscdk/src/app.ts
@@ -44,9 +44,18 @@ import { registerTokenResolver } from "@winglang/sdk/lib/core/tokens";
export interface CdkAppProps extends core.AppProps {
/**
* CDK Stack Name
- * @default - undefined
+ *
+ * @default - read from the CDK_STACK_NAME environment variable
*/
readonly stackName?: string;
+
+ /**
+ * A hook for customizating the way the root CDK stack is created. You can override this if you wish to use a custom stack
+ * instead of the default `cdk.Stack`.
+ *
+ * @default - creates a standard `cdk.Stack`
+ */
+ readonly stackFactory?: (app: cdk.App, stackName: string) => cdk.Stack;
}
/**
@@ -85,7 +94,9 @@ export class App extends core.App {
mkdirSync(cdkOutdir, { recursive: true });
const cdkApp = new cdk.App({ outdir: cdkOutdir });
- const cdkStack = new cdk.Stack(cdkApp, stackName);
+
+ const createStack = props.stackFactory ?? ((app, stackName) => new cdk.Stack(app, stackName));
+ const cdkStack = createStack(cdkApp, stackName);
super(cdkStack, props.rootId ?? "Default", props);
diff --git a/libs/awscdk/src/bucket.ts b/libs/awscdk/src/bucket.ts
index a99f0f4fc2c..d6d20585c98 100644
--- a/libs/awscdk/src/bucket.ts
+++ b/libs/awscdk/src/bucket.ts
@@ -10,11 +10,11 @@ import { BucketDeployment, Source } from "aws-cdk-lib/aws-s3-deployment";
import { LambdaDestination } from "aws-cdk-lib/aws-s3-notifications";
import { Construct } from "constructs";
import { App } from "./app";
-import { Function } from "./function";
import { cloud, core, std } from "@winglang/sdk";
import { convertBetweenHandlers } from "@winglang/sdk/lib/shared/convert";
import { calculateBucketPermissions } from "@winglang/sdk/lib/shared-aws/permissions";
import { IAwsBucket } from "@winglang/sdk/lib/shared-aws/bucket";
+import { IAwsCdkFunction, addPolicyStatements, isAwsCdkFunction } from "./function";
const EVENTS = {
[cloud.BucketEventType.DELETE]: EventType.OBJECT_REMOVED,
@@ -59,24 +59,22 @@ export class Bucket extends cloud.Bucket implements IAwsBucket {
event: string,
inflight: cloud.IBucketEventHandler,
opts?: cloud.BucketOnCreateOptions
- ): Function {
+ ): IAwsCdkFunction {
const functionHandler = convertBetweenHandlers(
inflight,
this.eventHandlerLocation(),
`BucketEventHandlerClient`
);
- const fn = new Function(
+ const fn = new cloud.Function(
this.node.scope!, // ok since we're not a tree root
App.of(this).makeId(this, `${this.node.id}-${event}`),
functionHandler,
opts
);
- if (!(fn instanceof Function)) {
- throw new Error(
- "Bucket only supports creating awscdk.Function right now"
- );
+ if (!isAwsCdkFunction(fn)) {
+ throw new Error("Expected function to implement IAwsCdkFunction");
}
return fn;
@@ -117,7 +115,7 @@ export class Bucket extends cloud.Bucket implements IAwsBucket {
this.bucket.addEventNotification(
EVENTS[cloud.BucketEventType.CREATE],
- new LambdaDestination(fn._function)
+ new LambdaDestination(fn.awscdkFunction)
);
}
@@ -135,7 +133,7 @@ export class Bucket extends cloud.Bucket implements IAwsBucket {
this.bucket.addEventNotification(
EVENTS[cloud.BucketEventType.DELETE],
- new LambdaDestination(fn._function)
+ new LambdaDestination(fn.awscdkFunction)
);
}
@@ -153,7 +151,7 @@ export class Bucket extends cloud.Bucket implements IAwsBucket {
this.bucket.addEventNotification(
EVENTS[cloud.BucketEventType.UPDATE],
- new LambdaDestination(fn._function)
+ new LambdaDestination(fn.awscdkFunction)
);
}
@@ -170,7 +168,7 @@ export class Bucket extends cloud.Bucket implements IAwsBucket {
});
this.bucket.addEventNotification(
EVENTS[cloud.BucketEventType.CREATE],
- new LambdaDestination(fn._function)
+ new LambdaDestination(fn.awscdkFunction)
);
std.Node.of(this).addConnection({
@@ -180,7 +178,7 @@ export class Bucket extends cloud.Bucket implements IAwsBucket {
});
this.bucket.addEventNotification(
EVENTS[cloud.BucketEventType.DELETE],
- new LambdaDestination(fn._function)
+ new LambdaDestination(fn.awscdkFunction)
);
std.Node.of(this).addConnection({
@@ -190,18 +188,16 @@ export class Bucket extends cloud.Bucket implements IAwsBucket {
});
this.bucket.addEventNotification(
EVENTS[cloud.BucketEventType.UPDATE],
- new LambdaDestination(fn._function)
+ new LambdaDestination(fn.awscdkFunction)
);
}
public onLift(host: std.IInflightHost, ops: string[]): void {
- if (!(host instanceof Function)) {
- throw new Error("buckets can only be bound by tfaws.Function for now");
+ if (!isAwsCdkFunction(host)) {
+ throw new Error("Expected 'host' to implement IAwsCdkFunction");
}
- host.addPolicyStatements(
- ...calculateBucketPermissions(this.bucket.bucketArn, ops)
- );
+ addPolicyStatements(host.awscdkFunction, calculateBucketPermissions(this.bucket.bucketArn, ops));
// The bucket name needs to be passed through an environment variable since
// it may not be resolved until deployment time.
diff --git a/libs/awscdk/src/counter.ts b/libs/awscdk/src/counter.ts
index 2f441d7f6be..e474c28bfb5 100644
--- a/libs/awscdk/src/counter.ts
+++ b/libs/awscdk/src/counter.ts
@@ -1,11 +1,11 @@
import { RemovalPolicy } from "aws-cdk-lib";
import { AttributeType, BillingMode, Table } from "aws-cdk-lib/aws-dynamodb";
import { Construct } from "constructs";
-import { Function } from "./function";
import { cloud, core, std } from "@winglang/sdk";
import { COUNTER_HASH_KEY } from "@winglang/sdk/lib/shared-aws/commons";
import { calculateCounterPermissions } from "@winglang/sdk/lib/shared-aws/permissions";
import { IAwsCounter } from "@winglang/sdk/lib/shared-aws/counter";
+import { addPolicyStatements, isAwsCdkFunction } from "./function";
/**
* AWS implementation of `cloud.Counter`.
@@ -36,13 +36,11 @@ export class Counter extends cloud.Counter implements IAwsCounter {
}
public onLift(host: std.IInflightHost, ops: string[]): void {
- if (!(host instanceof Function)) {
- throw new Error("counters can only be bound by awscdk.Function for now");
+ if (!isAwsCdkFunction(host)) {
+ throw new Error("Expected 'host' to implement 'isAwsCdkFunction' method");
}
- host.addPolicyStatements(
- ...calculateCounterPermissions(this.table.tableArn, ops)
- );
+ addPolicyStatements(host.awscdkFunction, calculateCounterPermissions(this.table.tableArn, ops));
host.addEnvironment(this.envName(), this.table.tableName);
diff --git a/libs/awscdk/src/dynamodb-table.ts b/libs/awscdk/src/dynamodb-table.ts
index b9b110a6f79..69346a9fdf7 100644
--- a/libs/awscdk/src/dynamodb-table.ts
+++ b/libs/awscdk/src/dynamodb-table.ts
@@ -1,7 +1,7 @@
import { RemovalPolicy } from "aws-cdk-lib";
-import { AttributeType, BillingMode, Table } from "aws-cdk-lib/aws-dynamodb";
+import { AttributeType, Billing, TableV2 } from "aws-cdk-lib/aws-dynamodb";
import { Construct } from "constructs";
-import { Function } from "./function";
+import { addPolicyStatements, isAwsCdkFunction } from "./function";
import { core, ex, std } from "@winglang/sdk";
import { ResourceNames } from "@winglang/sdk/lib/shared/resource-names";
import { IAwsDynamodbTable, NAME_OPTS } from "@winglang/sdk/lib/shared-aws/dynamodb-table";
@@ -13,14 +13,14 @@ import { calculateDynamodbTablePermissions } from "@winglang/sdk/lib/shared-aws/
* @inflight `@winglang/sdk.ex.IDynamodbTableClient`
*/
export class DynamodbTable extends ex.DynamodbTable implements IAwsDynamodbTable {
- private readonly table: Table;
+ private readonly table: TableV2;
constructor(scope: Construct, id: string, props: ex.DynamodbTableProps) {
super(scope, id, props);
const attributeDefinitions = props.attributeDefinitions as any;
- this.table = new Table(this, "Default", {
+ this.table = new TableV2(this, "Default", {
tableName: ResourceNames.generateName(this, {
prefix: this.name,
...NAME_OPTS,
@@ -35,21 +35,17 @@ export class DynamodbTable extends ex.DynamodbTable implements IAwsDynamodbTable
type: attributeDefinitions[props.rangeKey] as AttributeType,
}
: undefined,
- billingMode: BillingMode.PAY_PER_REQUEST,
+ billing: Billing.onDemand(),
removalPolicy: RemovalPolicy.DESTROY,
});
}
public onLift(host: std.IInflightHost, ops: string[]): void {
- if (!(host instanceof Function)) {
- throw new Error(
- "Dynamodb tables can only be bound by tfaws.Function for now"
- );
+ if (!isAwsCdkFunction(host)) {
+ throw new Error("Expected 'host' to implement 'isAwsCdkFunction' method");
}
- host.addPolicyStatements(
- ...calculateDynamodbTablePermissions(this.table.tableArn, ops)
- );
+ addPolicyStatements(host.awscdkFunction, calculateDynamodbTablePermissions(this.table.tableArn, ops));
host.addEnvironment(this.envName(), this.table.tableName);
diff --git a/libs/awscdk/src/endpoint.ts b/libs/awscdk/src/endpoint.ts
index 2edbb4ae1cd..63924a1d03f 100644
--- a/libs/awscdk/src/endpoint.ts
+++ b/libs/awscdk/src/endpoint.ts
@@ -16,12 +16,7 @@ export class Endpoint extends cloud.Endpoint {
/** @internal */
public onLift(host: std.IInflightHost, ops: string[]): void {
- if (!(host instanceof Function)) {
- throw new Error("endpoints can only be bound by awscdk.Function for now");
- }
-
host.addEnvironment(this.urlEnvName(), this.url);
-
super.onLift(host, ops);
}
diff --git a/libs/awscdk/src/function.ts b/libs/awscdk/src/function.ts
index aba1d6a2e61..5ac2a3311d8 100644
--- a/libs/awscdk/src/function.ts
+++ b/libs/awscdk/src/function.ts
@@ -4,27 +4,51 @@ import {
Architecture,
Function as CdkFunction,
Code,
- IEventSource,
Runtime,
} from "aws-cdk-lib/aws-lambda";
-import {
- LogGroup, RetentionDays
-} from "aws-cdk-lib/aws-logs";
+import { LogGroup, RetentionDays } from "aws-cdk-lib/aws-logs";
import { Asset } from "aws-cdk-lib/aws-s3-assets";
-import { Construct } from "constructs";
+import { Construct, IConstruct } from "constructs";
import { cloud, std, core } from "@winglang/sdk";
+import { NotImplementedError } from "@winglang/sdk/lib/core/errors";
import { createBundle } from "@winglang/sdk/lib/shared/bundling";
import { IAwsFunction, PolicyStatement } from "@winglang/sdk/lib/shared-aws";
import { resolve } from "path";
import { renameSync, rmSync, writeFileSync } from "fs";
import { App } from "./app";
+/**
+ * Implementation of `awscdk.Function` are expected to implement this
+ */
+export interface IAwsCdkFunction extends IConstruct {
+ awscdkFunction: CdkFunction;
+}
+
+export function isAwsCdkFunction(x: any): x is IAwsCdkFunction {
+ return typeof x["awscdkFunction"] === "object";
+}
+
+/**
+ * Adds a bunch of policy statements to the function's role.
+ */
+export function addPolicyStatements(
+ fn: CdkFunction,
+ statements: PolicyStatement[]
+) {
+ for (const statement of statements) {
+ fn.addToRolePolicy(new CdkPolicyStatement(statement));
+ }
+}
+
/**
* AWS implementation of `cloud.Function`.
*
* @inflight `@winglang/sdk.cloud.IFunctionClient`
*/
-export class Function extends cloud.Function implements IAwsFunction {
+export class Function
+ extends cloud.Function
+ implements IAwsCdkFunction, IAwsFunction
+{
private readonly function: CdkFunction;
private readonly assetPath: string;
@@ -36,6 +60,12 @@ export class Function extends cloud.Function implements IAwsFunction {
) {
super(scope, id, inflight, props);
+ if (props.concurrency != null) {
+ throw new NotImplementedError(
+ "Function concurrency isn't implemented yet on the current target."
+ );
+ }
+
// The code in `this.entrypoint` will be replaced during preSynthesize
// but we produce an initial version and bundle it so that `lambda.Function`
// has something to work with.
@@ -44,42 +74,19 @@ export class Function extends cloud.Function implements IAwsFunction {
writeFileSync(this.entrypoint, inflightCodeApproximation);
const bundle = createBundle(this.entrypoint);
- const logRetentionDays =
- props.logRetentionDays === undefined
- ? 30
- : props.logRetentionDays < 0
- ? RetentionDays.INFINITE // Negative value means Infinite retention
- : props.logRetentionDays;
-
const code = Code.fromAsset(resolve(bundle.directory));
- const logs = new LogGroup(this, "LogGroup", {
- retention: logRetentionDays
- });
-
- this.function = new CdkFunction(this, "Default", {
- handler: "index.handler",
- code,
- runtime: Runtime.NODEJS_20_X,
- environment: {
- NODE_OPTIONS: "--enable-source-maps",
- ...this.env
- },
- timeout: props.timeout
- ? Duration.seconds(props.timeout.seconds)
- : Duration.minutes(1),
- memorySize: props.memory ?? 1024,
- architecture: Architecture.ARM_64,
- logGroup: logs
- });
+ this.function = this.createFunction(code, props);
// hack: accessing private field from aws_lambda.AssetCode
// https://github.com/aws/aws-cdk/blob/109b2abe4c713624e731afa1b82c3c1a3ba064c9/packages/aws-cdk-lib/aws-lambda/lib/code.ts#L266
const asset: Asset = (code as any).asset;
if (!asset.assetPath) {
- throw new Error("AWS CDK 'Asset' class no longer has an 'assetPath' property");
+ throw new Error(
+ "AWS CDK 'Asset' class no longer has an 'assetPath' property"
+ );
}
- this.assetPath = asset.assetPath
+ this.assetPath = asset.assetPath;
}
/** @internal */
@@ -92,7 +99,7 @@ export class Function extends cloud.Function implements IAwsFunction {
// copy files from bundle.directory to this.assetPath
const assetDir = resolve(App.of(this).outdir, this.assetPath);
- rmSync(assetDir, { recursive: true, force: true })
+ rmSync(assetDir, { recursive: true, force: true });
renameSync(bundle.directory, assetDir);
}
@@ -105,15 +112,17 @@ export class Function extends cloud.Function implements IAwsFunction {
}
public onLift(host: std.IInflightHost, ops: string[]): void {
- if (!(host instanceof Function)) {
- throw new Error("functions can only be bound by awscdk.Function for now");
+ if (!isAwsCdkFunction(host)) {
+ throw new Error("Expected host to implement IAwsCdkFunction");
}
if (ops.includes(cloud.FunctionInflightMethods.INVOKE)) {
- host.addPolicyStatements({
- actions: ["lambda:InvokeFunction"],
- resources: [`${this.function.functionArn}`],
- });
+ host.awscdkFunction.addToRolePolicy(
+ new CdkPolicyStatement({
+ actions: ["lambda:InvokeFunction"],
+ resources: [`${this.function.functionArn}`],
+ })
+ );
}
// The function name needs to be passed through an environment variable since
@@ -125,12 +134,47 @@ export class Function extends cloud.Function implements IAwsFunction {
/** @internal */
public _toInflight(): string {
- return core.InflightClient.for(
- __dirname,
- __filename,
- "FunctionClient",
- [`process.env["${this.envName()}"], "${this.node.path}"`]
- );
+ return core.InflightClient.for(__dirname, __filename, "FunctionClient", [
+ `process.env["${this.envName()}"], "${this.node.path}"`,
+ ]);
+ }
+
+ /**
+ * Can be overridden by subclasses to customize the AWS CDK function creation.
+ * @param code The AWS Lambda `Code` object that represents the inflight closure defined for this function.
+ * @param props Cloud function properties.
+ * @returns an object that implements `aws-lambda.IFunction`.
+ */
+ protected createFunction(
+ code: Code,
+ props: cloud.FunctionProps
+ ): CdkFunction {
+ const logRetentionDays =
+ props.logRetentionDays === undefined
+ ? 30
+ : props.logRetentionDays < 0
+ ? RetentionDays.INFINITE // Negative value means Infinite retention
+ : props.logRetentionDays;
+
+ const logs = new LogGroup(this, "LogGroup", {
+ retention: logRetentionDays,
+ });
+
+ return new CdkFunction(this, "Default", {
+ handler: "index.handler",
+ code,
+ runtime: Runtime.NODEJS_20_X,
+ environment: {
+ NODE_OPTIONS: "--enable-source-maps",
+ ...this.env,
+ },
+ timeout: props.timeout
+ ? Duration.seconds(props.timeout.seconds)
+ : Duration.minutes(1),
+ memorySize: props.memory ?? 1024,
+ architecture: Architecture.ARM_64,
+ logGroup: logs,
+ });
}
/**
@@ -154,17 +198,11 @@ export class Function extends cloud.Function implements IAwsFunction {
}
}
- /** @internal */
- public _addEventSource(eventSource: IEventSource) {
- this.function.addEventSource(eventSource);
- }
-
private envName(): string {
return `FUNCTION_NAME_${this.node.addr.slice(-8)}`;
}
- /** @internal */
- get _function() {
+ public get awscdkFunction() {
return this.function;
}
diff --git a/libs/awscdk/src/on-deploy.ts b/libs/awscdk/src/on-deploy.ts
index 19ab1101f47..22b40f14ae1 100644
--- a/libs/awscdk/src/on-deploy.ts
+++ b/libs/awscdk/src/on-deploy.ts
@@ -1,7 +1,7 @@
import { Trigger } from "aws-cdk-lib/triggers";
import { Construct } from "constructs";
-import { Function as AwsFunction } from "./function";
import { cloud, core } from "@winglang/sdk";
+import { isAwsCdkFunction } from "./function";
/**
* AWS implementation of `cloud.OnDeploy`.
@@ -18,10 +18,13 @@ export class OnDeploy extends cloud.OnDeploy {
super(scope, id, handler, props);
let fn = new cloud.Function(this, "Function", handler as cloud.IFunctionHandler, props);
- const awsFn = fn as AwsFunction;
+
+ if (!isAwsCdkFunction(fn)) {
+ throw new Error("Expected function to implement 'IAwsCdkFunction' method");
+ }
let trigger = new Trigger(this, "Trigger", {
- handler: awsFn._function,
+ handler: fn.awscdkFunction,
});
trigger.executeAfter(...(props.executeAfter ?? []));
diff --git a/libs/awscdk/src/queue.ts b/libs/awscdk/src/queue.ts
index 4a9d4092326..248fe7b3ca2 100644
--- a/libs/awscdk/src/queue.ts
+++ b/libs/awscdk/src/queue.ts
@@ -3,12 +3,12 @@ import { Duration } from "aws-cdk-lib";
import { SqsEventSource } from "aws-cdk-lib/aws-lambda-event-sources";
import { Queue as SQSQueue } from "aws-cdk-lib/aws-sqs";
import { Construct } from "constructs";
-import { Function } from "./function";
import { App } from "./app";
import { std, core, cloud } from "@winglang/sdk";
import { convertBetweenHandlers } from "@winglang/sdk/lib/shared/convert";
import { calculateQueuePermissions } from "@winglang/sdk/lib/shared-aws/permissions";
import { IAwsQueue } from "@winglang/sdk/lib/shared-aws/queue";
+import { addPolicyStatements, isAwsCdkFunction } from "./function";
/**
* AWS implementation of `cloud.Queue`.
@@ -46,7 +46,7 @@ export class Queue extends cloud.Queue implements IAwsQueue {
"QueueSetConsumerHandlerClient"
);
- const fn = new Function(
+ const fn = new cloud.Function(
// ok since we're not a tree root
this.node.scope!,
App.of(this).makeId(this, `${this.node.id}-SetConsumer`),
@@ -57,15 +57,15 @@ export class Queue extends cloud.Queue implements IAwsQueue {
}
);
- // TODO: remove this constraint by adding generic permission APIs to cloud.Function
- if (!(fn instanceof Function)) {
- throw new Error("Queue only supports creating awscdk.Function right now");
+ if (!isAwsCdkFunction(fn)) {
+ throw new Error("Queue only supports creating IAwsCdkFunction right now");
}
const eventSource = new SqsEventSource(this.queue, {
batchSize: props.batchSize ?? 1,
});
- fn._addEventSource(eventSource);
+
+ fn.awscdkFunction.addEventSource(eventSource);
std.Node.of(this).addConnection({
source: this,
@@ -87,15 +87,13 @@ export class Queue extends cloud.Queue implements IAwsQueue {
}
public onLift(host: std.IInflightHost, ops: string[]): void {
- if (!(host instanceof Function)) {
- throw new Error("queues can only be bound by tfaws.Function for now");
+ if (!isAwsCdkFunction(host)) {
+ throw new Error("Expected 'host' to implement IAwsCdkFunction");
}
const env = this.envName();
- host.addPolicyStatements(
- ...calculateQueuePermissions(this.queue.queueArn, ops)
- );
+ addPolicyStatements(host.awscdkFunction, calculateQueuePermissions(this.queue.queueArn, ops));
// The queue url needs to be passed through an environment variable since
// it may not be resolved until deployment time.
diff --git a/libs/awscdk/src/schedule.ts b/libs/awscdk/src/schedule.ts
index a992ed84ee2..6bdb8471ccb 100644
--- a/libs/awscdk/src/schedule.ts
+++ b/libs/awscdk/src/schedule.ts
@@ -6,10 +6,10 @@ import {
addLambdaPermission,
} from "aws-cdk-lib/aws-events-targets";
import { Construct } from "constructs";
-import { Function } from "./function";
import { App } from "./app";
import { cloud, core, std } from "@winglang/sdk";
import { convertBetweenHandlers } from "@winglang/sdk/lib/shared/convert";
+import { isAwsCdkFunction } from "./function";
/**
* AWS implementation of `cloud.Schedule`.
@@ -73,7 +73,7 @@ export class Schedule extends cloud.Schedule {
"ScheduleOnTickHandlerClient"
);
- const fn = new Function(
+ const fn = new cloud.Function(
// ok since we're not a tree root
this.node.scope!,
App.of(this).makeId(this, `${this.node.id}-OnTick`),
@@ -81,15 +81,12 @@ export class Schedule extends cloud.Schedule {
props
);
- // TODO: remove this constraint by adding generic permission APIs to cloud.Function
- if (!(fn instanceof Function)) {
- throw new Error(
- "Schedule only supports creating awscdk.Function right now"
- );
+ if (!isAwsCdkFunction(fn)) {
+ throw new Error("Expected function to implement 'isAwsCdkFunction' method");
}
- this.rule.addTarget(new LambdaFunction(fn._function));
- addLambdaPermission(this.rule, fn._function);
+ this.rule.addTarget(new LambdaFunction(fn.awscdkFunction));
+ addLambdaPermission(this.rule, fn.awscdkFunction);
std.Node.of(this).addConnection({
source: this,
diff --git a/libs/awscdk/src/secret.ts b/libs/awscdk/src/secret.ts
index 027e01f462a..5e660fffa0f 100644
--- a/libs/awscdk/src/secret.ts
+++ b/libs/awscdk/src/secret.ts
@@ -4,7 +4,7 @@ import {
Secret as CdkSecret,
} from "aws-cdk-lib/aws-secretsmanager";
import { Construct } from "constructs";
-import { Function } from "./function";
+import { addPolicyStatements, isAwsCdkFunction } from "./function";
import { cloud, core, std } from "@winglang/sdk";
import { calculateSecretPermissions } from "@winglang/sdk/lib/shared-aws/permissions";
@@ -48,13 +48,11 @@ export class Secret extends cloud.Secret {
}
public onLift(host: std.IInflightHost, ops: string[]): void {
- if (!(host instanceof Function)) {
- throw new Error("secrets can only be bound by awscdk.Function for now");
+ if (!isAwsCdkFunction(host)) {
+ throw new Error("Expected 'host' to implement 'isAwsCdkFunction' method");
}
- host.addPolicyStatements(
- ...calculateSecretPermissions(this.arnForPolicies, ops)
- );
+ addPolicyStatements(host.awscdkFunction, calculateSecretPermissions(this.arnForPolicies, ops));
host.addEnvironment(this.envName(), this.secret.secretArn);
diff --git a/libs/awscdk/src/test-runner.ts b/libs/awscdk/src/test-runner.ts
index 30b6c9c0d5a..23915daea7c 100644
--- a/libs/awscdk/src/test-runner.ts
+++ b/libs/awscdk/src/test-runner.ts
@@ -1,7 +1,7 @@
import { CfnOutput, Lazy } from "aws-cdk-lib";
import { Construct } from "constructs";
-import { Function as AwsFunction } from "./function";
import { core, std } from "@winglang/sdk";
+import { isAwsCdkFunction } from "./function";
const OUTPUT_TEST_RUNNER_FUNCTION_ARNS = "WingTestRunnerFunctionArns";
@@ -28,10 +28,6 @@ export class TestRunner extends std.TestRunner {
}
public onLift(host: std.IInflightHost, ops: string[]): void {
- if (!(host instanceof AwsFunction)) {
- throw new Error("TestRunner can only be bound by tfaws.Function for now");
- }
-
// Collect all of the test functions and their ARNs, and pass them to the
// test engine so they can be invoked inflight.
// TODO: are we going to run into AWS's 4KB environment variable limit here?
@@ -66,12 +62,12 @@ export class TestRunner extends std.TestRunner {
const arns = new Map();
for (const test of this.findTests()) {
if (test._fn) {
- if (!(test._fn instanceof AwsFunction)) {
+ if (!(isAwsCdkFunction(test._fn))) {
throw new Error(
`Unsupported test function type, ${test._fn.node.path} was not a tfaws.Function`
);
}
- arns.set(test.node.path, (test._fn as AwsFunction).functionArn);
+ arns.set(test.node.path, test._fn.awscdkFunction.functionArn);
}
}
return arns;
diff --git a/libs/awscdk/src/tokens.ts b/libs/awscdk/src/tokens.ts
index 34247080433..6184860ad0a 100644
--- a/libs/awscdk/src/tokens.ts
+++ b/libs/awscdk/src/tokens.ts
@@ -1,7 +1,7 @@
import { Fn, Token } from "aws-cdk-lib";
-import { Function } from "@winglang/sdk/lib/cloud";
import { tokenEnvName, ITokenResolver } from "@winglang/sdk/lib/core/tokens";
import { IInflightHost } from "@winglang/sdk/lib/std";
+import { isAwsCdkFunction } from "./function";
/**
* Represents values that can only be resolved after the app is synthesized.
@@ -41,8 +41,8 @@ export class CdkTokens implements ITokenResolver {
* Binds the given token to the host.
*/
public onLiftValue(host: IInflightHost, value: any) {
- if (!(host instanceof Function)) {
- throw new Error(`Tokens can only be bound by a Function for now`);
+ if (!isAwsCdkFunction(host)) {
+ throw new Error("Expected 'host' to implement 'isAwsCdkFunction' method");
}
let envValue;
@@ -65,9 +65,8 @@ export class CdkTokens implements ITokenResolver {
}
const envName = tokenEnvName(value.toString());
+
// the same token might be bound multiple times by different variables/inflight contexts
- if (host.env[envName] === undefined) {
- host.addEnvironment(envName, envValue);
- }
+ host.addEnvironment(envName, envValue);
}
}
diff --git a/libs/awscdk/src/topic.ts b/libs/awscdk/src/topic.ts
index 77d594cb808..93a5b6758b0 100644
--- a/libs/awscdk/src/topic.ts
+++ b/libs/awscdk/src/topic.ts
@@ -2,12 +2,12 @@ import { join } from "path";
import { Topic as SNSTopic } from "aws-cdk-lib/aws-sns";
import { LambdaSubscription } from "aws-cdk-lib/aws-sns-subscriptions";
import { Construct } from "constructs";
-import { Function } from "./function";
import { App } from "./app";
import { cloud, core, std } from "@winglang/sdk";
import { convertBetweenHandlers } from "@winglang/sdk/lib/shared/convert";
import { calculateTopicPermissions } from "@winglang/sdk/lib/shared-aws/permissions";
import { IAwsTopic } from "@winglang/sdk/lib/shared-aws/topic";
+import { addPolicyStatements, isAwsCdkFunction } from "./function";
/**
* AWS Implementation of `cloud.Topic`.
@@ -35,19 +35,18 @@ export class Topic extends cloud.Topic implements IAwsTopic {
"TopicOnMessageHandlerClient"
);
- const fn = new Function(
+ const fn = new cloud.Function(
this.node.scope!, // ok since we're not a tree root
App.of(this).makeId(this, `${this.node.id}-OnMessage`),
functionHandler,
props
);
- // TODO: remove this constraint by adding geric permission APIs to cloud.Function
- if (!(fn instanceof Function)) {
- throw new Error("Topic only supports creating awscdk.Function right now");
+ if (!isAwsCdkFunction(fn)) {
+ throw new Error("Expected function to implement 'IAwsCdkFunction' method");
}
- const subscription = new LambdaSubscription(fn._function);
+ const subscription = new LambdaSubscription(fn.awscdkFunction);
this.topic.addSubscription(subscription);
std.Node.of(this).addConnection({
@@ -60,13 +59,11 @@ export class Topic extends cloud.Topic implements IAwsTopic {
}
public onLift(host: std.IInflightHost, ops: string[]): void {
- if (!(host instanceof Function)) {
- throw new Error("topics can only be bound by awscdk.Function for now");
+ if (!isAwsCdkFunction(host)) {
+ throw new Error("Expected 'host' to implement 'IAwsCdkFunction' method");
}
- host.addPolicyStatements(
- ...calculateTopicPermissions(this.topic.topicArn, ops)
- );
+ addPolicyStatements(host.awscdkFunction, calculateTopicPermissions(this.topic.topicArn, ops));
host.addEnvironment(this.envName(), this.topic.topicArn);
diff --git a/libs/awscdk/test/__snapshots__/dynamodb-table.test.ts.snap b/libs/awscdk/test/__snapshots__/dynamodb-table.test.ts.snap
index d3c8ecd5ca7..6dad8f4b2bd 100644
--- a/libs/awscdk/test/__snapshots__/dynamodb-table.test.ts.snap
+++ b/libs/awscdk/test/__snapshots__/dynamodb-table.test.ts.snap
@@ -26,9 +26,16 @@ exports[`default dynamodb table behavior 1`] = `
"KeyType": "HASH",
},
],
+ "Replicas": [
+ {
+ "Region": {
+ "Ref": "AWS::Region",
+ },
+ },
+ ],
"TableName": "my-wing-tableTable-c85e6383",
},
- "Type": "AWS::DynamoDB::Table",
+ "Type": "AWS::DynamoDB::GlobalTable",
"UpdateReplacePolicy": "Delete",
},
},
@@ -204,9 +211,16 @@ exports[`function with a table binding 1`] = `
"KeyType": "HASH",
},
],
+ "Replicas": [
+ {
+ "Region": {
+ "Ref": "AWS::Region",
+ },
+ },
+ ],
"TableName": "my-wing-tableTable-c85e6383",
},
- "Type": "AWS::DynamoDB::Table",
+ "Type": "AWS::DynamoDB::GlobalTable",
"UpdateReplacePolicy": "Delete",
},
},
diff --git a/libs/awscdk/test/app.test.ts b/libs/awscdk/test/app.test.ts
new file mode 100644
index 00000000000..408ca6a415b
--- /dev/null
+++ b/libs/awscdk/test/app.test.ts
@@ -0,0 +1,48 @@
+import { test, expect } from "vitest";
+import * as awscdk from "../src";
+import { CDK_APP_OPTS } from "./util";
+import { Duration, Stack } from "aws-cdk-lib";
+import { mkdtemp } from "@winglang/sdk/test/util";
+import { cloud, simulator } from "@winglang/sdk";
+import { Code, Function, Runtime } from "aws-cdk-lib/aws-lambda";
+
+test("custom stack", async () => {
+ const app = new awscdk.App({
+ ...CDK_APP_OPTS,
+ outdir: mkdtemp(),
+ stackFactory: (app, stackName) => {
+ return new Stack(app, stackName, {
+ description: "This is a custom stack description"
+ });
+ }
+ });
+
+ const out = JSON.parse(app.synth());
+ expect(out.Description, "This is a custom stack description");
+});
+
+test("custom Functions", async () => {
+ const app = new awscdk.App({
+ ...CDK_APP_OPTS,
+ outdir: mkdtemp(),
+ });
+
+ class CustomFunction extends awscdk.Function {
+ protected createFunction(code: Code, props: cloud.FunctionProps): Function {
+ return new Function(this, "Function", {
+ code,
+ handler: "index.handler",
+ runtime: Runtime.NODEJS_LATEST,
+ environment: {
+ BOOM: "BAR"
+ }
+ });
+ }
+ }
+
+ new CustomFunction(app, "MyFunction", simulator.Testing.makeHandler("async handle(name) { console.log('hello'); }"));
+
+ const cfn = JSON.parse(app.synth());
+
+ expect(cfn.Resources.MyFunctionDBE6350A.Properties.Environment.Variables).toStrictEqual({ BOOM: 'BAR' });
+});
\ No newline at end of file
diff --git a/libs/awscdk/test/bucket.test.ts b/libs/awscdk/test/bucket.test.ts
index 208ca4949bd..4c212a43739 100644
--- a/libs/awscdk/test/bucket.test.ts
+++ b/libs/awscdk/test/bucket.test.ts
@@ -3,12 +3,7 @@ import { test, expect } from "vitest";
import { cloud, simulator } from "@winglang/sdk";
import * as awscdk from "../src";
import { mkdtemp } from "@winglang/sdk/test/util";
-import { awscdkSanitize } from "./util";
-
-const CDK_APP_OPTS = {
- stackName: "my-project",
- entrypointDir: __dirname,
-};
+import { awscdkSanitize, CDK_APP_OPTS } from "./util";
test("create a bucket", async () => {
// GIVEN
diff --git a/libs/awscdk/test/counter.test.ts b/libs/awscdk/test/counter.test.ts
index 613b3494f63..5611c43c04e 100644
--- a/libs/awscdk/test/counter.test.ts
+++ b/libs/awscdk/test/counter.test.ts
@@ -3,12 +3,7 @@ import { test, expect } from "vitest";
import { cloud, simulator } from "@winglang/sdk";
import * as awscdk from "../src";
import { mkdtemp } from "@winglang/sdk/test/util";
-import { sanitizeCode, awscdkSanitize } from "./util";
-
-const CDK_APP_OPTS = {
- stackName: "my-project",
- entrypointDir: __dirname,
-};
+import { sanitizeCode, awscdkSanitize, CDK_APP_OPTS } from "./util";
test("default counter behavior", () => {
const app = new awscdk.App({ outdir: mkdtemp(), ...CDK_APP_OPTS });
diff --git a/libs/awscdk/test/dynamodb-table.test.ts b/libs/awscdk/test/dynamodb-table.test.ts
index 38d73b10825..a9e05f309db 100644
--- a/libs/awscdk/test/dynamodb-table.test.ts
+++ b/libs/awscdk/test/dynamodb-table.test.ts
@@ -1,14 +1,9 @@
-import { Match, Template } from "aws-cdk-lib/assertions";
+import { Template } from "aws-cdk-lib/assertions";
import { test, expect } from "vitest";
import { cloud, simulator, ex } from "@winglang/sdk";
import * as awscdk from "../src";
import { mkdtemp } from "@winglang/sdk/test/util";
-import { awscdkSanitize } from "./util";
-
-const CDK_APP_OPTS = {
- stackName: "my-project",
- entrypointDir: __dirname,
-};
+import { awscdkSanitize, CDK_APP_OPTS } from "./util";
test("default dynamodb table behavior", () => {
// GIVEN
@@ -22,7 +17,7 @@ test("default dynamodb table behavior", () => {
// THEN
const template = Template.fromJSON(JSON.parse(output));
- template.hasResource("AWS::DynamoDB::Table", 1);
+ template.hasResource("AWS::DynamoDB::GlobalTable", 1);
expect(awscdkSanitize(template)).toMatchSnapshot();
});
@@ -52,7 +47,7 @@ test("function with a table binding", () => {
const template = Template.fromJSON(JSON.parse(output));
template.resourceCountIs("AWS::Logs::LogGroup", 1);
- template.hasResource("AWS::DynamoDB::Table", 1);
+ template.hasResource("AWS::DynamoDB::GlobalTable", 1);
template.hasResource("AWS::IAM::Role", 1);
template.hasResource("AWS::IAM::Policy", 1);
template.hasResource("AWS::Lambda::Function", 1);
diff --git a/libs/awscdk/test/function.test.ts b/libs/awscdk/test/function.test.ts
index ec6dcb20620..c93e7cdce46 100644
--- a/libs/awscdk/test/function.test.ts
+++ b/libs/awscdk/test/function.test.ts
@@ -3,12 +3,7 @@ import { test, expect } from "vitest";
import { cloud, simulator, std } from "@winglang/sdk";
import * as awscdk from "../src";
import { mkdtemp } from "@winglang/sdk/test/util";
-import { awscdkSanitize } from "./util";
-
-const CDK_APP_OPTS = {
- stackName: "my-project",
- entrypointDir: __dirname,
-};
+import { awscdkSanitize, CDK_APP_OPTS } from "./util";
const INFLIGHT_CODE = `async handle(name) { console.log("Hello, " + name); }`;
diff --git a/libs/awscdk/test/on-deploy.test.ts b/libs/awscdk/test/on-deploy.test.ts
index 48d94936c3f..3c2f95b22f2 100644
--- a/libs/awscdk/test/on-deploy.test.ts
+++ b/libs/awscdk/test/on-deploy.test.ts
@@ -3,11 +3,7 @@ import { expect, test } from "vitest";
import { cloud, simulator } from "@winglang/sdk";
import * as awscdk from "../src";
import { mkdtemp } from "@winglang/sdk/test/util";
-import { awscdkSanitize } from "./util";
-
-const CDK_APP_OPTS = {
- stackName: "my-project",
-};
+import { awscdkSanitize, CDK_APP_OPTS } from "./util";
const INFLIGHT_CODE = `async handle(name) { console.log("Hello, " + name); }`;
@@ -15,7 +11,6 @@ test("create an OnDeploy", () => {
// GIVEN
const app = new awscdk.App({
outdir: mkdtemp(),
- entrypointDir: __dirname,
...CDK_APP_OPTS,
});
const handler = simulator.Testing.makeHandler(INFLIGHT_CODE);
@@ -32,7 +27,6 @@ test("execute OnDeploy after other resources", () => {
// GIVEN
const app = new awscdk.App({
outdir: mkdtemp(),
- entrypointDir: __dirname,
...CDK_APP_OPTS,
});
const bucket = new cloud.Bucket(app, "my_bucket");
@@ -55,7 +49,6 @@ test("execute OnDeploy before other resources", () => {
// GIVEN
const app = new awscdk.App({
outdir: mkdtemp(),
- entrypointDir: __dirname,
...CDK_APP_OPTS,
});
const bucket = new cloud.Bucket(app, "my_bucket");
diff --git a/libs/awscdk/test/queue.test.ts b/libs/awscdk/test/queue.test.ts
index abca96e738f..641b43abb0e 100644
--- a/libs/awscdk/test/queue.test.ts
+++ b/libs/awscdk/test/queue.test.ts
@@ -3,12 +3,7 @@ import { test, expect } from "vitest";
import { std, simulator, cloud } from "@winglang/sdk";
import * as awscdk from "../src";
import { mkdtemp } from "@winglang/sdk/test/util";
-import { sanitizeCode, awscdkSanitize } from "./util";
-
-const CDK_APP_OPTS = {
- stackName: "my-project",
- entrypointDir: __dirname,
-};
+import { sanitizeCode, awscdkSanitize, CDK_APP_OPTS } from "./util";
test("default queue behavior", () => {
// GIVEN
diff --git a/libs/awscdk/test/schedule.test.ts b/libs/awscdk/test/schedule.test.ts
index 6ec765850dd..5092f3c4f30 100644
--- a/libs/awscdk/test/schedule.test.ts
+++ b/libs/awscdk/test/schedule.test.ts
@@ -3,12 +3,7 @@ import { test, expect } from "vitest";
import { simulator, cloud, std } from "@winglang/sdk";
import * as awscdk from "../src";
import { mkdtemp } from "@winglang/sdk/test/util";
-import { awscdkSanitize } from "./util";
-
-const CDK_APP_OPTS = {
- stackName: "my-project",
- entrypointDir: __dirname,
-};
+import { awscdkSanitize, CDK_APP_OPTS } from "./util";
test("schedule behavior with rate", () => {
// GIVEN
diff --git a/libs/awscdk/test/secret.test.ts b/libs/awscdk/test/secret.test.ts
index d30a34e0639..af636c5b280 100644
--- a/libs/awscdk/test/secret.test.ts
+++ b/libs/awscdk/test/secret.test.ts
@@ -4,11 +4,7 @@ import { test, expect } from "vitest";
import { cloud } from "@winglang/sdk";
import * as awscdk from "../src";
import { mkdtemp } from "@winglang/sdk/test/util";
-
-const CDK_APP_OPTS = {
- stackName: "my-project",
- entrypointDir: __dirname,
-};
+import { CDK_APP_OPTS} from "./util";
test("default secret behavior", () => {
// GIVEN
diff --git a/libs/awscdk/test/topic.test.ts b/libs/awscdk/test/topic.test.ts
index 56a24d74544..50c8c288933 100644
--- a/libs/awscdk/test/topic.test.ts
+++ b/libs/awscdk/test/topic.test.ts
@@ -3,12 +3,7 @@ import { test, expect } from "vitest";
import { cloud, simulator } from "@winglang/sdk";
import * as awscdk from "../src";
import { mkdtemp } from "@winglang/sdk/test/util";
-import { sanitizeCode, awscdkSanitize } from "./util";
-
-const CDK_APP_OPTS = {
- stackName: "my-project",
- entrypointDir: __dirname,
-};
+import { sanitizeCode, awscdkSanitize, CDK_APP_OPTS } from "./util";
test("default topic behavior", () => {
// GIVEN
diff --git a/libs/awscdk/test/util.ts b/libs/awscdk/test/util.ts
index ff0f03c53a2..d57deadd97c 100644
--- a/libs/awscdk/test/util.ts
+++ b/libs/awscdk/test/util.ts
@@ -31,4 +31,9 @@ export function awscdkSanitize(template: Template): any {
);
return JSON.parse(jsonString);
-}
\ No newline at end of file
+}
+
+export const CDK_APP_OPTS = {
+ stackName: "my-project",
+ entrypointDir: __dirname,
+};
diff --git a/libs/tree-sitter-wing/grammar.js b/libs/tree-sitter-wing/grammar.js
index c7f9b3709fe..7bfe12e3485 100644
--- a/libs/tree-sitter-wing/grammar.js
+++ b/libs/tree-sitter-wing/grammar.js
@@ -396,7 +396,7 @@ module.exports = grammar({
/[0-7]{1,3}/,
/x[0-9a-fA-F]{2}/,
/u[0-9a-fA-F]{4}/,
- /u{[0-9a-fA-F]+}/
+ /u\{[0-9a-fA-F]+\}/
)
)
),
diff --git a/libs/wingc/src/jsify.rs b/libs/wingc/src/jsify.rs
index 1d30850256e..1dda49a9edc 100644
--- a/libs/wingc/src/jsify.rs
+++ b/libs/wingc/src/jsify.rs
@@ -508,7 +508,11 @@ impl<'a> JSifier<'a> {
Some(if let Some(id_exp) = obj_id {
self.jsify_expression(id_exp, ctx).to_string()
} else {
- format!("\"{}\"", ctor.to_string())
+ // take only the last part of the fully qualified name (the class name) because any
+ // leading parts like the namespace are volatile and can be changed easily by the user
+ let s = ctor.to_string();
+ let class_name = s.split(".").last().unwrap().to_string();
+ format!("\"{}\"", class_name)
})
} else {
None
@@ -1211,11 +1215,9 @@ impl<'a> JSifier<'a> {
for value in values {
code.line(new_code!(
&value.span,
- "tmp[tmp[\"",
+ "tmp[\"",
jsify_symbol(value),
- "\"] = ",
- value_index.to_string(),
- "] = \",",
+ "\"] = \"",
jsify_symbol(value),
"\";"
));
diff --git a/libs/wingc/src/jsify/snapshots/base_class_lift_indirect.snap b/libs/wingc/src/jsify/snapshots/base_class_lift_indirect.snap
index cf66c39f334..f78004d09d4 100644
--- a/libs/wingc/src/jsify/snapshots/base_class_lift_indirect.snap
+++ b/libs/wingc/src/jsify/snapshots/base_class_lift_indirect.snap
@@ -83,7 +83,7 @@ class $Root extends $stdlib.std.Resource {
class Base extends $stdlib.std.Resource {
constructor($scope, $id, ) {
super($scope, $id);
- this.b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "cloud.Bucket");
+ this.b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket");
}
static _toInflightType() {
return `
diff --git a/libs/wingc/src/jsify/snapshots/base_class_with_lifted_field_object.snap b/libs/wingc/src/jsify/snapshots/base_class_with_lifted_field_object.snap
index 1c70d586f2c..d7ebc177c6c 100644
--- a/libs/wingc/src/jsify/snapshots/base_class_with_lifted_field_object.snap
+++ b/libs/wingc/src/jsify/snapshots/base_class_with_lifted_field_object.snap
@@ -74,7 +74,7 @@ class $Root extends $stdlib.std.Resource {
class Base extends $stdlib.std.Resource {
constructor($scope, $id, ) {
super($scope, $id);
- this.b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "cloud.Bucket");
+ this.b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket");
}
static _toInflightType() {
return `
diff --git a/libs/wingc/src/jsify/snapshots/calls_methods_on_preflight_object.snap b/libs/wingc/src/jsify/snapshots/calls_methods_on_preflight_object.snap
index c577e6eabe6..53f3b82011e 100644
--- a/libs/wingc/src/jsify/snapshots/calls_methods_on_preflight_object.snap
+++ b/libs/wingc/src/jsify/snapshots/calls_methods_on_preflight_object.snap
@@ -86,7 +86,7 @@ class $Root extends $stdlib.std.Resource {
});
}
}
- const b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "cloud.Bucket");
+ const b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket");
this.node.root.new("@winglang/sdk.std.Test", std.Test, this, "test:test", new $Closure1(this, "$Closure1"));
}
}
diff --git a/libs/wingc/src/jsify/snapshots/capture_identifier_from_preflight_scope_with_nested_object.snap b/libs/wingc/src/jsify/snapshots/capture_identifier_from_preflight_scope_with_nested_object.snap
index 697366362be..aa81640b27d 100644
--- a/libs/wingc/src/jsify/snapshots/capture_identifier_from_preflight_scope_with_nested_object.snap
+++ b/libs/wingc/src/jsify/snapshots/capture_identifier_from_preflight_scope_with_nested_object.snap
@@ -74,7 +74,7 @@ class $Root extends $stdlib.std.Resource {
class Foo extends $stdlib.std.Resource {
constructor($scope, $id, ) {
super($scope, $id);
- this.b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "cloud.Bucket");
+ this.b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket");
}
static _toInflightType() {
return `
diff --git a/libs/wingc/src/jsify/snapshots/capture_object_with_this_in_name.snap b/libs/wingc/src/jsify/snapshots/capture_object_with_this_in_name.snap
index ce6e538abc7..f7db052f9b9 100644
--- a/libs/wingc/src/jsify/snapshots/capture_object_with_this_in_name.snap
+++ b/libs/wingc/src/jsify/snapshots/capture_object_with_this_in_name.snap
@@ -83,7 +83,7 @@ class $Root extends $stdlib.std.Resource {
});
}
}
- const bucket_this = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "cloud.Bucket");
+ const bucket_this = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket");
const fn = new $Closure1(this, "$Closure1");
}
}
diff --git a/libs/wingc/src/jsify/snapshots/capture_token.snap b/libs/wingc/src/jsify/snapshots/capture_token.snap
index 3df441b6f16..29ea132fdc4 100644
--- a/libs/wingc/src/jsify/snapshots/capture_token.snap
+++ b/libs/wingc/src/jsify/snapshots/capture_token.snap
@@ -82,7 +82,7 @@ class $Root extends $stdlib.std.Resource {
});
}
}
- const api = this.node.root.new("@winglang/sdk.cloud.Api", cloud.Api, this, "cloud.Api");
+ const api = this.node.root.new("@winglang/sdk.cloud.Api", cloud.Api, this, "Api");
this.node.root.new("@winglang/sdk.std.Test", std.Test, this, "test:test", new $Closure1(this, "$Closure1"));
}
}
diff --git a/libs/wingc/src/jsify/snapshots/closure_field.snap b/libs/wingc/src/jsify/snapshots/closure_field.snap
index 6d2b5a57cf5..33cb54eb787 100644
--- a/libs/wingc/src/jsify/snapshots/closure_field.snap
+++ b/libs/wingc/src/jsify/snapshots/closure_field.snap
@@ -214,7 +214,7 @@ class $Root extends $stdlib.std.Resource {
});
}
}
- const globalBucket = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "cloud.Bucket");
+ const globalBucket = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket");
const x = new MyResource(this, "MyResource");
this.node.root.new("@winglang/sdk.std.Test", std.Test, this, "test:variable can be an inflight closure", new $Closure2(this, "$Closure2"));
}
diff --git a/libs/wingc/src/jsify/snapshots/enum_value.snap b/libs/wingc/src/jsify/snapshots/enum_value.snap
index 492ba875019..e8e39f2798f 100644
--- a/libs/wingc/src/jsify/snapshots/enum_value.snap
+++ b/libs/wingc/src/jsify/snapshots/enum_value.snap
@@ -52,8 +52,8 @@ class $Root extends $stdlib.std.Resource {
super($scope, $id);
const MyEnum =
(function (tmp) {
- tmp[tmp["B"] = 0] = ",B";
- tmp[tmp["C"] = 1] = ",C";
+ tmp["B"] = "B";
+ tmp["C"] = "C";
return tmp;
})({})
;
diff --git a/libs/wingc/src/jsify/snapshots/identify_field.snap b/libs/wingc/src/jsify/snapshots/identify_field.snap
index 80e1ee53e7a..abd10c04ccc 100644
--- a/libs/wingc/src/jsify/snapshots/identify_field.snap
+++ b/libs/wingc/src/jsify/snapshots/identify_field.snap
@@ -54,7 +54,7 @@ class $Root extends $stdlib.std.Resource {
class A extends $stdlib.std.Resource {
constructor($scope, $id, ) {
super($scope, $id);
- this.bucket_this = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "cloud.Bucket");
+ this.bucket_this = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket");
}
static _toInflightType() {
return `
diff --git a/libs/wingc/src/jsify/snapshots/implicit_lift_inflight_init.snap b/libs/wingc/src/jsify/snapshots/implicit_lift_inflight_init.snap
index ffc90570b92..b87e9d42683 100644
--- a/libs/wingc/src/jsify/snapshots/implicit_lift_inflight_init.snap
+++ b/libs/wingc/src/jsify/snapshots/implicit_lift_inflight_init.snap
@@ -72,7 +72,7 @@ class $Root extends $stdlib.std.Resource {
class Foo extends $stdlib.std.Resource {
constructor($scope, $id, ) {
super($scope, $id);
- this.c = this.node.root.new("@winglang/sdk.cloud.Counter", cloud.Counter, this, "cloud.Counter");
+ this.c = this.node.root.new("@winglang/sdk.cloud.Counter", cloud.Counter, this, "Counter");
}
static _toInflightType() {
return `
diff --git a/libs/wingc/src/jsify/snapshots/indirect_capture.snap b/libs/wingc/src/jsify/snapshots/indirect_capture.snap
index 332e4322f70..1eee7ecb268 100644
--- a/libs/wingc/src/jsify/snapshots/indirect_capture.snap
+++ b/libs/wingc/src/jsify/snapshots/indirect_capture.snap
@@ -87,7 +87,7 @@ class $Root extends $stdlib.std.Resource {
class Capture extends $stdlib.std.Resource {
constructor($scope, $id, ) {
super($scope, $id);
- this.q = this.node.root.new("@winglang/sdk.cloud.Queue", cloud.Queue, this, "cloud.Queue");
+ this.q = this.node.root.new("@winglang/sdk.cloud.Queue", cloud.Queue, this, "Queue");
}
static _toInflightType() {
return `
@@ -156,7 +156,7 @@ class $Root extends $stdlib.std.Resource {
});
}
}
- const b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "cloud.Bucket");
+ const b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket");
const f = new Capture(this, "Capture");
this.node.root.new("@winglang/sdk.std.Test", std.Test, this, "test:test", new $Closure1(this, "$Closure1"));
}
diff --git a/libs/wingc/src/jsify/snapshots/inline_inflight_class.snap b/libs/wingc/src/jsify/snapshots/inline_inflight_class.snap
index 916228372d4..8ba6c02c1f8 100644
--- a/libs/wingc/src/jsify/snapshots/inline_inflight_class.snap
+++ b/libs/wingc/src/jsify/snapshots/inline_inflight_class.snap
@@ -75,8 +75,8 @@ class $Root extends $stdlib.std.Resource {
class Foo extends $stdlib.std.Resource {
constructor($scope, $id, ) {
super($scope, $id);
- this.b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "cloud.Bucket");
- this.q = this.node.root.new("@winglang/sdk.cloud.Queue", cloud.Queue, this, "cloud.Queue");
+ this.b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket");
+ this.q = this.node.root.new("@winglang/sdk.cloud.Queue", cloud.Queue, this, "Queue");
const __parent_this_1 = this;
class $Closure1 extends $stdlib.std.AutoIdResource {
_id = $stdlib.core.closureId();
diff --git a/libs/wingc/src/jsify/snapshots/lift_element_from_collection_as_field.snap b/libs/wingc/src/jsify/snapshots/lift_element_from_collection_as_field.snap
index f524d978861..87aa5717c2a 100644
--- a/libs/wingc/src/jsify/snapshots/lift_element_from_collection_as_field.snap
+++ b/libs/wingc/src/jsify/snapshots/lift_element_from_collection_as_field.snap
@@ -55,7 +55,7 @@ class $Root extends $stdlib.std.Resource {
class Foo extends $stdlib.std.Resource {
constructor($scope, $id, ) {
super($scope, $id);
- this.arr = [this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "cloud.Bucket")];
+ this.arr = [this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket")];
}
static _toInflightType() {
return `
diff --git a/libs/wingc/src/jsify/snapshots/lift_element_from_collection_of_objects.snap b/libs/wingc/src/jsify/snapshots/lift_element_from_collection_of_objects.snap
index 7945d0ccb18..cff66f9c6ec 100644
--- a/libs/wingc/src/jsify/snapshots/lift_element_from_collection_of_objects.snap
+++ b/libs/wingc/src/jsify/snapshots/lift_element_from_collection_of_objects.snap
@@ -84,7 +84,7 @@ class $Root extends $stdlib.std.Resource {
});
}
}
- const a = [this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "cloud.Bucket")];
+ const a = [this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket")];
this.node.root.new("@winglang/sdk.std.Test", std.Test, this, "test:test", new $Closure1(this, "$Closure1"));
}
}
diff --git a/libs/wingc/src/jsify/snapshots/lift_via_closure.snap b/libs/wingc/src/jsify/snapshots/lift_via_closure.snap
index 088c6133bc9..57b650951db 100644
--- a/libs/wingc/src/jsify/snapshots/lift_via_closure.snap
+++ b/libs/wingc/src/jsify/snapshots/lift_via_closure.snap
@@ -145,7 +145,7 @@ class $Root extends $stdlib.std.Resource {
});
}
}
- const bucket = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "cloud.Bucket");
+ const bucket = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket");
const fn = new $Closure1(this, "$Closure1");
this.node.root.new("@winglang/sdk.std.Test", std.Test, this, "test:test", new $Closure2(this, "$Closure2"));
}
diff --git a/libs/wingc/src/jsify/snapshots/lift_via_closure_class_explicit.snap b/libs/wingc/src/jsify/snapshots/lift_via_closure_class_explicit.snap
index 8d874725446..28db92b97cf 100644
--- a/libs/wingc/src/jsify/snapshots/lift_via_closure_class_explicit.snap
+++ b/libs/wingc/src/jsify/snapshots/lift_via_closure_class_explicit.snap
@@ -94,7 +94,7 @@ class $Root extends $stdlib.std.Resource {
_id = $stdlib.core.closureId();
constructor($scope, $id, ) {
super($scope, $id);
- this.q = this.node.root.new("@winglang/sdk.cloud.Queue", cloud.Queue, this, "cloud.Queue");
+ this.q = this.node.root.new("@winglang/sdk.cloud.Queue", cloud.Queue, this, "Queue");
}
static _toInflightType() {
return `
diff --git a/libs/wingc/src/jsify/snapshots/nested_preflight_operation.snap b/libs/wingc/src/jsify/snapshots/nested_preflight_operation.snap
index d9a50654485..deea62f439d 100644
--- a/libs/wingc/src/jsify/snapshots/nested_preflight_operation.snap
+++ b/libs/wingc/src/jsify/snapshots/nested_preflight_operation.snap
@@ -95,7 +95,7 @@ class $Root extends $stdlib.std.Resource {
class YourType extends $stdlib.std.Resource {
constructor($scope, $id, ) {
super($scope, $id);
- this.b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "cloud.Bucket");
+ this.b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket");
}
static _toInflightType() {
return `
diff --git a/libs/wingc/src/jsify/snapshots/preflight_nested_object_with_operations.snap b/libs/wingc/src/jsify/snapshots/preflight_nested_object_with_operations.snap
index af08fbe99b6..394c0eb17c1 100644
--- a/libs/wingc/src/jsify/snapshots/preflight_nested_object_with_operations.snap
+++ b/libs/wingc/src/jsify/snapshots/preflight_nested_object_with_operations.snap
@@ -135,7 +135,7 @@ class $Root extends $stdlib.std.Resource {
});
}
}
- const b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "cloud.Bucket");
+ const b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket");
const a = new A(this, "A");
this.node.root.new("@winglang/sdk.std.Test", std.Test, this, "test:test", new $Closure1(this, "$Closure1"));
}
diff --git a/libs/wingc/src/jsify/snapshots/preflight_object_through_property.snap b/libs/wingc/src/jsify/snapshots/preflight_object_through_property.snap
index 1b4608057c9..47e41e41fa6 100644
--- a/libs/wingc/src/jsify/snapshots/preflight_object_through_property.snap
+++ b/libs/wingc/src/jsify/snapshots/preflight_object_through_property.snap
@@ -75,7 +75,7 @@ class $Root extends $stdlib.std.Resource {
class MyType extends $stdlib.std.Resource {
constructor($scope, $id, ) {
super($scope, $id);
- this.b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "cloud.Bucket");
+ this.b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket");
}
static _toInflightType() {
return `
diff --git a/libs/wingc/src/jsify/snapshots/preflight_object_with_operations.snap b/libs/wingc/src/jsify/snapshots/preflight_object_with_operations.snap
index ddb280c4a9d..2af7944c3cb 100644
--- a/libs/wingc/src/jsify/snapshots/preflight_object_with_operations.snap
+++ b/libs/wingc/src/jsify/snapshots/preflight_object_with_operations.snap
@@ -85,7 +85,7 @@ class $Root extends $stdlib.std.Resource {
});
}
}
- const b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "cloud.Bucket");
+ const b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket");
this.node.root.new("@winglang/sdk.std.Test", std.Test, this, "test:test", new $Closure1(this, "$Closure1"));
}
}
diff --git a/libs/wingc/src/jsify/snapshots/preflight_object_with_operations_multiple_methods.snap b/libs/wingc/src/jsify/snapshots/preflight_object_with_operations_multiple_methods.snap
index eb1970f75b5..dc66c3c19c9 100644
--- a/libs/wingc/src/jsify/snapshots/preflight_object_with_operations_multiple_methods.snap
+++ b/libs/wingc/src/jsify/snapshots/preflight_object_with_operations_multiple_methods.snap
@@ -91,7 +91,7 @@ class $Root extends $stdlib.std.Resource {
});
}
}
- const b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "cloud.Bucket");
+ const b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket");
}
}
const $PlatformManager = new $stdlib.platform.PlatformManager({platformPaths: $platforms});
diff --git a/libs/wingc/src/jsify/snapshots/reference_preflight_fields.snap b/libs/wingc/src/jsify/snapshots/reference_preflight_fields.snap
index e388418ee39..acf17e637c4 100644
--- a/libs/wingc/src/jsify/snapshots/reference_preflight_fields.snap
+++ b/libs/wingc/src/jsify/snapshots/reference_preflight_fields.snap
@@ -72,7 +72,7 @@ class $Root extends $stdlib.std.Resource {
constructor($scope, $id, ) {
super($scope, $id);
this.s = "hello";
- this.b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "cloud.Bucket");
+ this.b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket");
}
static _toInflightType() {
return `
diff --git a/libs/wingc/src/jsify/snapshots/reference_preflight_free_variable_with_this_in_the_expression.snap b/libs/wingc/src/jsify/snapshots/reference_preflight_free_variable_with_this_in_the_expression.snap
index f483d3e0269..4e0c6271f7f 100644
--- a/libs/wingc/src/jsify/snapshots/reference_preflight_free_variable_with_this_in_the_expression.snap
+++ b/libs/wingc/src/jsify/snapshots/reference_preflight_free_variable_with_this_in_the_expression.snap
@@ -92,7 +92,7 @@ class $Root extends $stdlib.std.Resource {
});
}
}
- const b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "cloud.Bucket");
+ const b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket");
}
}
const $PlatformManager = new $stdlib.platform.PlatformManager({platformPaths: $platforms});
diff --git a/libs/wingc/src/jsify/snapshots/reference_preflight_object_from_static_inflight.snap b/libs/wingc/src/jsify/snapshots/reference_preflight_object_from_static_inflight.snap
index c556815c1ff..32c5abb62aa 100644
--- a/libs/wingc/src/jsify/snapshots/reference_preflight_object_from_static_inflight.snap
+++ b/libs/wingc/src/jsify/snapshots/reference_preflight_object_from_static_inflight.snap
@@ -86,7 +86,7 @@ class $Root extends $stdlib.std.Resource {
});
}
}
- const q = this.node.root.new("@winglang/sdk.cloud.Queue", cloud.Queue, this, "cloud.Queue");
+ const q = this.node.root.new("@winglang/sdk.cloud.Queue", cloud.Queue, this, "Queue");
}
}
const $PlatformManager = new $stdlib.platform.PlatformManager({platformPaths: $platforms});
diff --git a/libs/wingc/src/jsify/snapshots/reference_static_inflight_which_references_preflight_object.snap b/libs/wingc/src/jsify/snapshots/reference_static_inflight_which_references_preflight_object.snap
index e16ed3c57f1..7564cb78703 100644
--- a/libs/wingc/src/jsify/snapshots/reference_static_inflight_which_references_preflight_object.snap
+++ b/libs/wingc/src/jsify/snapshots/reference_static_inflight_which_references_preflight_object.snap
@@ -148,7 +148,7 @@ class $Root extends $stdlib.std.Resource {
});
}
}
- const b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "cloud.Bucket");
+ const b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket");
this.node.root.new("@winglang/sdk.std.Test", std.Test, this, "test:test", new $Closure1(this, "$Closure1"));
}
}
diff --git a/libs/wingc/src/jsify/snapshots/static_inflight_operation.snap b/libs/wingc/src/jsify/snapshots/static_inflight_operation.snap
index cef20fdbe78..46f1fb8fd2a 100644
--- a/libs/wingc/src/jsify/snapshots/static_inflight_operation.snap
+++ b/libs/wingc/src/jsify/snapshots/static_inflight_operation.snap
@@ -145,7 +145,7 @@ class $Root extends $stdlib.std.Resource {
});
}
}
- const b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "cloud.Bucket");
+ const b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket");
this.node.root.new("@winglang/sdk.std.Test", std.Test, this, "test:test", new $Closure1(this, "$Closure1"));
}
}
diff --git a/libs/wingc/src/jsify/snapshots/transitive_reference.snap b/libs/wingc/src/jsify/snapshots/transitive_reference.snap
index 1f83e8d00f2..9f015c038ca 100644
--- a/libs/wingc/src/jsify/snapshots/transitive_reference.snap
+++ b/libs/wingc/src/jsify/snapshots/transitive_reference.snap
@@ -94,7 +94,7 @@ class $Root extends $stdlib.std.Resource {
class MyType extends $stdlib.std.Resource {
constructor($scope, $id, ) {
super($scope, $id);
- this.b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "cloud.Bucket");
+ this.b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket");
}
static _toInflightType() {
return `
diff --git a/libs/wingc/src/jsify/snapshots/transitive_reference_via_inflight_class.snap b/libs/wingc/src/jsify/snapshots/transitive_reference_via_inflight_class.snap
index 3c6d69f7a4c..9a2ab0825ff 100644
--- a/libs/wingc/src/jsify/snapshots/transitive_reference_via_inflight_class.snap
+++ b/libs/wingc/src/jsify/snapshots/transitive_reference_via_inflight_class.snap
@@ -140,7 +140,7 @@ class $Root extends $stdlib.std.Resource {
});
}
}
- const b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "cloud.Bucket");
+ const b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket");
this.node.root.new("@winglang/sdk.std.Test", std.Test, this, "test:test", new $Closure1(this, "$Closure1"));
}
}
diff --git a/libs/wingc/src/jsify/snapshots/transitive_reference_via_static.snap b/libs/wingc/src/jsify/snapshots/transitive_reference_via_static.snap
index 9e0635e9298..270c246026b 100644
--- a/libs/wingc/src/jsify/snapshots/transitive_reference_via_static.snap
+++ b/libs/wingc/src/jsify/snapshots/transitive_reference_via_static.snap
@@ -204,7 +204,7 @@ class $Root extends $stdlib.std.Resource {
});
}
}
- const b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "cloud.Bucket");
+ const b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket");
const t = new YourType(this, "YourType");
this.node.root.new("@winglang/sdk.std.Test", std.Test, this, "test:test", new $Closure1(this, "$Closure1"));
}
diff --git a/libs/wingc/src/jsify/snapshots/two_identical_lifts.snap b/libs/wingc/src/jsify/snapshots/two_identical_lifts.snap
index a47cb9ac5a6..8c0bf845e5f 100644
--- a/libs/wingc/src/jsify/snapshots/two_identical_lifts.snap
+++ b/libs/wingc/src/jsify/snapshots/two_identical_lifts.snap
@@ -91,7 +91,7 @@ class $Root extends $stdlib.std.Resource {
});
}
}
- const b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "cloud.Bucket");
+ const b = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket");
this.node.root.new("@winglang/sdk.std.Test", std.Test, this, "test:test", new $Closure1(this, "$Closure1"));
}
}
diff --git a/libs/wingc/src/lib.rs b/libs/wingc/src/lib.rs
index 0f3ea243447..36850203ce6 100644
--- a/libs/wingc/src/lib.rs
+++ b/libs/wingc/src/lib.rs
@@ -20,6 +20,7 @@ use jsify::JSifier;
use lifting::LiftVisitor;
use parser::{is_entrypoint_file, parse_wing_project};
+use serde::Serialize;
use struct_schema::StructSchemaVisitor;
use type_check::jsii_importer::JsiiImportSpec;
use type_check::symbol_env::SymbolEnvKind;
@@ -37,7 +38,7 @@ use std::mem;
use crate::ast::Phase;
use crate::type_check::symbol_env::SymbolEnv;
-use crate::type_check::{TypeChecker, Types};
+use crate::type_check::{SymbolEnvOrNamespace, TypeChecker, Types};
#[macro_use]
#[cfg(test)]
@@ -124,7 +125,10 @@ const MACRO_REPLACE_ARGS_TEXT: &'static str = "$args_text$";
pub const TRUSTED_LIBRARY_NPM_NAMESPACE: &'static str = "@winglibs";
-pub struct CompilerOutput {}
+#[derive(Serialize)]
+pub struct CompilerOutput {
+ imported_namespaces: Vec,
+}
/// Exposes an allocation function to the WASM host
///
@@ -200,10 +204,11 @@ pub unsafe extern "C" fn wingc_compile(ptr: u32, len: u32) -> u64 {
}
let results = compile(project_dir, source_path, None, output_dir);
+
if results.is_err() {
WASM_RETURN_ERROR
} else {
- string_to_combined_ptr("".to_string())
+ string_to_combined_ptr(serde_json::to_string(&results.unwrap()).unwrap())
}
}
@@ -378,7 +383,16 @@ pub fn compile(
return Err(());
}
- return Ok(CompilerOutput {});
+ let imported_namespaces = types
+ .source_file_envs
+ .iter()
+ .filter_map(|(k, v)| match v {
+ SymbolEnvOrNamespace::Namespace(_) => Some(k.to_string()),
+ _ => None,
+ })
+ .collect::>();
+
+ return Ok(CompilerOutput { imported_namespaces });
}
pub fn is_absolute_path(path: &Utf8Path) -> bool {
diff --git a/libs/wingc/src/lsp/snapshots/completions/incomplete_inflight_namespace.snap b/libs/wingc/src/lsp/snapshots/completions/incomplete_inflight_namespace.snap
index a121951c269..bfe32f53fca 100644
--- a/libs/wingc/src/lsp/snapshots/completions/incomplete_inflight_namespace.snap
+++ b/libs/wingc/src/lsp/snapshots/completions/incomplete_inflight_namespace.snap
@@ -35,13 +35,13 @@ source: libs/wingc/src/lsp/completions.rs
kind: 7
documentation:
kind: markdown
- value: "```wing\nclass Function impl IInflightHost\n```\n---\nA function.\n\n### Initializer\n- `handler` — `inflight (event: str?): str?`\n- `...props` — `FunctionProps?`\n \n - `env?` — `Map?` — Environment variables to pass to the function.\n - `logRetentionDays?` — `num?` — Specifies the number of days that function logs will be kept.\n - `memory?` — `num?` — The amount of memory to allocate to the function, in MB.\n - `timeout?` — `duration?` — The maximum amount of time the function can run.\n### Fields\n- `env` — `Map` — Returns the set of environment variables for this function.\n- `node` — `Node` — The tree node.\n### Methods\n- `addEnvironment` — `preflight (name: str, value: str): void` — Add an environment variable to the function.\n- `invoke` — `inflight (payload: str?): str?` — Invokes the function with a payload and waits for the result.\n- `invokeAsync` — `inflight (payload: str?): void` — Kicks off the execution of the function with a payload and returns immediately while the function is running.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `toString` — `preflight (): str` — Returns a string representation of this construct."
+ value: "```wing\nclass Function impl IInflightHost\n```\n---\nA function.\n\n### Initializer\n- `handler` — `inflight (event: str?): str?`\n- `...props` — `FunctionProps?`\n \n - `concurrency?` — `num?` — The maximum concurrent invocations that can run at one time.\n - `env?` — `Map?` — Environment variables to pass to the function.\n - `logRetentionDays?` — `num?` — Specifies the number of days that function logs will be kept.\n - `memory?` — `num?` — The amount of memory to allocate to the function, in MB.\n - `timeout?` — `duration?` — The maximum amount of time the function can run.\n### Fields\n- `env` — `Map` — Returns the set of environment variables for this function.\n- `node` — `Node` — The tree node.\n### Methods\n- `addEnvironment` — `preflight (name: str, value: str): void` — Add an environment variable to the function.\n- `invoke` — `inflight (payload: str?): str?` — Invokes the function with a payload and waits for the result.\n- `invokeAsync` — `inflight (payload: str?): void` — Kicks off the execution of the function with a payload and returns immediately while the function is running.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `toString` — `preflight (): str` — Returns a string representation of this construct."
sortText: gg|Function
- label: OnDeploy
kind: 7
documentation:
kind: markdown
- value: "```wing\nclass OnDeploy\n```\n---\nRun code every time the app is deployed.\n\n### Initializer\n- `handler` — `inflight (): void`\n- `...props` — `OnDeployProps?`\n \n - `env?` — `Map?`\n - `executeAfter?` — `Array?` — Execute this trigger only after these resources have been provisioned.\n - `executeBefore?` — `Array?` — Adds this trigger as a dependency on other constructs.\n - `logRetentionDays?` — `num?`\n - `memory?` — `num?`\n - `timeout?` — `duration?`\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `toString` — `preflight (): str` — Returns a string representation of this construct."
+ value: "```wing\nclass OnDeploy\n```\n---\nRun code every time the app is deployed.\n\n### Initializer\n- `handler` — `inflight (): void`\n- `...props` — `OnDeployProps?`\n \n - `concurrency?` — `num?`\n - `env?` — `Map?`\n - `executeAfter?` — `Array?` — Execute this trigger only after these resources have been provisioned.\n - `executeBefore?` — `Array?` — Adds this trigger as a dependency on other constructs.\n - `logRetentionDays?` — `num?`\n - `memory?` — `num?`\n - `timeout?` — `duration?`\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `toString` — `preflight (): str` — Returns a string representation of this construct."
sortText: gg|OnDeploy
- label: Queue
kind: 7
@@ -245,7 +245,7 @@ source: libs/wingc/src/lsp/completions.rs
kind: 22
documentation:
kind: markdown
- value: "```wing\nstruct FunctionProps\n```\n---\nOptions for `Function`.\n### Fields\n- `env?` — `Map?` — Environment variables to pass to the function.\n- `logRetentionDays?` — `num?` — Specifies the number of days that function logs will be kept.\n- `memory?` — `num?` — The amount of memory to allocate to the function, in MB.\n- `timeout?` — `duration?` — The maximum amount of time the function can run."
+ value: "```wing\nstruct FunctionProps\n```\n---\nOptions for `Function`.\n### Fields\n- `concurrency?` — `num?` — The maximum concurrent invocations that can run at one time.\n- `env?` — `Map?` — Environment variables to pass to the function.\n- `logRetentionDays?` — `num?` — Specifies the number of days that function logs will be kept.\n- `memory?` — `num?` — The amount of memory to allocate to the function, in MB.\n- `timeout?` — `duration?` — The maximum amount of time the function can run."
sortText: hh|FunctionProps
- label: GetSecretValueOptions
kind: 22
@@ -263,7 +263,7 @@ source: libs/wingc/src/lsp/completions.rs
kind: 22
documentation:
kind: markdown
- value: "```wing\nstruct OnDeployProps extends FunctionProps\n```\n---\nOptions for `OnDeploy`.\n### Fields\n- `env?` — `Map?`\n- `executeAfter?` — `Array?` — Execute this trigger only after these resources have been provisioned.\n- `executeBefore?` — `Array?` — Adds this trigger as a dependency on other constructs.\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
+ value: "```wing\nstruct OnDeployProps extends FunctionProps\n```\n---\nOptions for `OnDeploy`.\n### Fields\n- `concurrency?` — `num?`\n- `env?` — `Map?`\n- `executeAfter?` — `Array?` — Execute this trigger only after these resources have been provisioned.\n- `executeBefore?` — `Array?` — Adds this trigger as a dependency on other constructs.\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
sortText: hh|OnDeployProps
- label: QueueProps
kind: 22
@@ -275,13 +275,13 @@ source: libs/wingc/src/lsp/completions.rs
kind: 22
documentation:
kind: markdown
- value: "```wing\nstruct QueueSetConsumerOptions extends FunctionProps\n```\n---\nOptions for Queue.setConsumer.\n### Fields\n- `batchSize?` — `num?` — The maximum number of messages to send to subscribers at once.\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
+ value: "```wing\nstruct QueueSetConsumerOptions extends FunctionProps\n```\n---\nOptions for Queue.setConsumer.\n### Fields\n- `batchSize?` — `num?` — The maximum number of messages to send to subscribers at once.\n- `concurrency?` — `num?`\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
sortText: hh|QueueSetConsumerOptions
- label: ScheduleOnTickOptions
kind: 22
documentation:
kind: markdown
- value: "```wing\nstruct ScheduleOnTickOptions extends FunctionProps\n```\n---\nOptions for Schedule.onTick.\n### Fields\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
+ value: "```wing\nstruct ScheduleOnTickOptions extends FunctionProps\n```\n---\nOptions for Schedule.onTick.\n### Fields\n- `concurrency?` — `num?`\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
sortText: hh|ScheduleOnTickOptions
- label: ScheduleProps
kind: 22
@@ -299,7 +299,7 @@ source: libs/wingc/src/lsp/completions.rs
kind: 22
documentation:
kind: markdown
- value: "```wing\nstruct ServiceOnStartOptions extends FunctionProps\n```\n---\nOptions for Service.onStart.\n### Fields\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
+ value: "```wing\nstruct ServiceOnStartOptions extends FunctionProps\n```\n---\nOptions for Service.onStart.\n### Fields\n- `concurrency?` — `num?`\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
sortText: hh|ServiceOnStartOptions
- label: ServiceProps
kind: 22
@@ -311,7 +311,7 @@ source: libs/wingc/src/lsp/completions.rs
kind: 22
documentation:
kind: markdown
- value: "```wing\nstruct TopicOnMessageOptions extends FunctionProps\n```\n---\nOptions for `Topic.onMessage`.\n### Fields\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
+ value: "```wing\nstruct TopicOnMessageOptions extends FunctionProps\n```\n---\nOptions for `Topic.onMessage`.\n### Fields\n- `concurrency?` — `num?`\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
sortText: hh|TopicOnMessageOptions
- label: TopicProps
kind: 22
diff --git a/libs/wingc/src/lsp/snapshots/completions/namespace_middle_dot.snap b/libs/wingc/src/lsp/snapshots/completions/namespace_middle_dot.snap
index a121951c269..bfe32f53fca 100644
--- a/libs/wingc/src/lsp/snapshots/completions/namespace_middle_dot.snap
+++ b/libs/wingc/src/lsp/snapshots/completions/namespace_middle_dot.snap
@@ -35,13 +35,13 @@ source: libs/wingc/src/lsp/completions.rs
kind: 7
documentation:
kind: markdown
- value: "```wing\nclass Function impl IInflightHost\n```\n---\nA function.\n\n### Initializer\n- `handler` — `inflight (event: str?): str?`\n- `...props` — `FunctionProps?`\n \n - `env?` — `Map?` — Environment variables to pass to the function.\n - `logRetentionDays?` — `num?` — Specifies the number of days that function logs will be kept.\n - `memory?` — `num?` — The amount of memory to allocate to the function, in MB.\n - `timeout?` — `duration?` — The maximum amount of time the function can run.\n### Fields\n- `env` — `Map` — Returns the set of environment variables for this function.\n- `node` — `Node` — The tree node.\n### Methods\n- `addEnvironment` — `preflight (name: str, value: str): void` — Add an environment variable to the function.\n- `invoke` — `inflight (payload: str?): str?` — Invokes the function with a payload and waits for the result.\n- `invokeAsync` — `inflight (payload: str?): void` — Kicks off the execution of the function with a payload and returns immediately while the function is running.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `toString` — `preflight (): str` — Returns a string representation of this construct."
+ value: "```wing\nclass Function impl IInflightHost\n```\n---\nA function.\n\n### Initializer\n- `handler` — `inflight (event: str?): str?`\n- `...props` — `FunctionProps?`\n \n - `concurrency?` — `num?` — The maximum concurrent invocations that can run at one time.\n - `env?` — `Map?` — Environment variables to pass to the function.\n - `logRetentionDays?` — `num?` — Specifies the number of days that function logs will be kept.\n - `memory?` — `num?` — The amount of memory to allocate to the function, in MB.\n - `timeout?` — `duration?` — The maximum amount of time the function can run.\n### Fields\n- `env` — `Map` — Returns the set of environment variables for this function.\n- `node` — `Node` — The tree node.\n### Methods\n- `addEnvironment` — `preflight (name: str, value: str): void` — Add an environment variable to the function.\n- `invoke` — `inflight (payload: str?): str?` — Invokes the function with a payload and waits for the result.\n- `invokeAsync` — `inflight (payload: str?): void` — Kicks off the execution of the function with a payload and returns immediately while the function is running.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `toString` — `preflight (): str` — Returns a string representation of this construct."
sortText: gg|Function
- label: OnDeploy
kind: 7
documentation:
kind: markdown
- value: "```wing\nclass OnDeploy\n```\n---\nRun code every time the app is deployed.\n\n### Initializer\n- `handler` — `inflight (): void`\n- `...props` — `OnDeployProps?`\n \n - `env?` — `Map?`\n - `executeAfter?` — `Array?` — Execute this trigger only after these resources have been provisioned.\n - `executeBefore?` — `Array?` — Adds this trigger as a dependency on other constructs.\n - `logRetentionDays?` — `num?`\n - `memory?` — `num?`\n - `timeout?` — `duration?`\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `toString` — `preflight (): str` — Returns a string representation of this construct."
+ value: "```wing\nclass OnDeploy\n```\n---\nRun code every time the app is deployed.\n\n### Initializer\n- `handler` — `inflight (): void`\n- `...props` — `OnDeployProps?`\n \n - `concurrency?` — `num?`\n - `env?` — `Map?`\n - `executeAfter?` — `Array?` — Execute this trigger only after these resources have been provisioned.\n - `executeBefore?` — `Array?` — Adds this trigger as a dependency on other constructs.\n - `logRetentionDays?` — `num?`\n - `memory?` — `num?`\n - `timeout?` — `duration?`\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `toString` — `preflight (): str` — Returns a string representation of this construct."
sortText: gg|OnDeploy
- label: Queue
kind: 7
@@ -245,7 +245,7 @@ source: libs/wingc/src/lsp/completions.rs
kind: 22
documentation:
kind: markdown
- value: "```wing\nstruct FunctionProps\n```\n---\nOptions for `Function`.\n### Fields\n- `env?` — `Map?` — Environment variables to pass to the function.\n- `logRetentionDays?` — `num?` — Specifies the number of days that function logs will be kept.\n- `memory?` — `num?` — The amount of memory to allocate to the function, in MB.\n- `timeout?` — `duration?` — The maximum amount of time the function can run."
+ value: "```wing\nstruct FunctionProps\n```\n---\nOptions for `Function`.\n### Fields\n- `concurrency?` — `num?` — The maximum concurrent invocations that can run at one time.\n- `env?` — `Map?` — Environment variables to pass to the function.\n- `logRetentionDays?` — `num?` — Specifies the number of days that function logs will be kept.\n- `memory?` — `num?` — The amount of memory to allocate to the function, in MB.\n- `timeout?` — `duration?` — The maximum amount of time the function can run."
sortText: hh|FunctionProps
- label: GetSecretValueOptions
kind: 22
@@ -263,7 +263,7 @@ source: libs/wingc/src/lsp/completions.rs
kind: 22
documentation:
kind: markdown
- value: "```wing\nstruct OnDeployProps extends FunctionProps\n```\n---\nOptions for `OnDeploy`.\n### Fields\n- `env?` — `Map?`\n- `executeAfter?` — `Array?` — Execute this trigger only after these resources have been provisioned.\n- `executeBefore?` — `Array?` — Adds this trigger as a dependency on other constructs.\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
+ value: "```wing\nstruct OnDeployProps extends FunctionProps\n```\n---\nOptions for `OnDeploy`.\n### Fields\n- `concurrency?` — `num?`\n- `env?` — `Map?`\n- `executeAfter?` — `Array?` — Execute this trigger only after these resources have been provisioned.\n- `executeBefore?` — `Array?` — Adds this trigger as a dependency on other constructs.\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
sortText: hh|OnDeployProps
- label: QueueProps
kind: 22
@@ -275,13 +275,13 @@ source: libs/wingc/src/lsp/completions.rs
kind: 22
documentation:
kind: markdown
- value: "```wing\nstruct QueueSetConsumerOptions extends FunctionProps\n```\n---\nOptions for Queue.setConsumer.\n### Fields\n- `batchSize?` — `num?` — The maximum number of messages to send to subscribers at once.\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
+ value: "```wing\nstruct QueueSetConsumerOptions extends FunctionProps\n```\n---\nOptions for Queue.setConsumer.\n### Fields\n- `batchSize?` — `num?` — The maximum number of messages to send to subscribers at once.\n- `concurrency?` — `num?`\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
sortText: hh|QueueSetConsumerOptions
- label: ScheduleOnTickOptions
kind: 22
documentation:
kind: markdown
- value: "```wing\nstruct ScheduleOnTickOptions extends FunctionProps\n```\n---\nOptions for Schedule.onTick.\n### Fields\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
+ value: "```wing\nstruct ScheduleOnTickOptions extends FunctionProps\n```\n---\nOptions for Schedule.onTick.\n### Fields\n- `concurrency?` — `num?`\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
sortText: hh|ScheduleOnTickOptions
- label: ScheduleProps
kind: 22
@@ -299,7 +299,7 @@ source: libs/wingc/src/lsp/completions.rs
kind: 22
documentation:
kind: markdown
- value: "```wing\nstruct ServiceOnStartOptions extends FunctionProps\n```\n---\nOptions for Service.onStart.\n### Fields\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
+ value: "```wing\nstruct ServiceOnStartOptions extends FunctionProps\n```\n---\nOptions for Service.onStart.\n### Fields\n- `concurrency?` — `num?`\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
sortText: hh|ServiceOnStartOptions
- label: ServiceProps
kind: 22
@@ -311,7 +311,7 @@ source: libs/wingc/src/lsp/completions.rs
kind: 22
documentation:
kind: markdown
- value: "```wing\nstruct TopicOnMessageOptions extends FunctionProps\n```\n---\nOptions for `Topic.onMessage`.\n### Fields\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
+ value: "```wing\nstruct TopicOnMessageOptions extends FunctionProps\n```\n---\nOptions for `Topic.onMessage`.\n### Fields\n- `concurrency?` — `num?`\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
sortText: hh|TopicOnMessageOptions
- label: TopicProps
kind: 22
diff --git a/libs/wingc/src/lsp/snapshots/completions/new_expression_nested.snap b/libs/wingc/src/lsp/snapshots/completions/new_expression_nested.snap
index a959ed7534d..8ccc2e0fadd 100644
--- a/libs/wingc/src/lsp/snapshots/completions/new_expression_nested.snap
+++ b/libs/wingc/src/lsp/snapshots/completions/new_expression_nested.snap
@@ -60,7 +60,7 @@ source: libs/wingc/src/lsp/completions.rs
kind: 7
documentation:
kind: markdown
- value: "```wing\nclass Function impl IInflightHost\n```\n---\nA function.\n\n### Initializer\n- `handler` — `inflight (event: str?): str?`\n- `...props` — `FunctionProps?`\n \n - `env?` — `Map?` — Environment variables to pass to the function.\n - `logRetentionDays?` — `num?` — Specifies the number of days that function logs will be kept.\n - `memory?` — `num?` — The amount of memory to allocate to the function, in MB.\n - `timeout?` — `duration?` — The maximum amount of time the function can run.\n### Fields\n- `env` — `Map` — Returns the set of environment variables for this function.\n- `node` — `Node` — The tree node.\n### Methods\n- `addEnvironment` — `preflight (name: str, value: str): void` — Add an environment variable to the function.\n- `invoke` — `inflight (payload: str?): str?` — Invokes the function with a payload and waits for the result.\n- `invokeAsync` — `inflight (payload: str?): void` — Kicks off the execution of the function with a payload and returns immediately while the function is running.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `toString` — `preflight (): str` — Returns a string representation of this construct."
+ value: "```wing\nclass Function impl IInflightHost\n```\n---\nA function.\n\n### Initializer\n- `handler` — `inflight (event: str?): str?`\n- `...props` — `FunctionProps?`\n \n - `concurrency?` — `num?` — The maximum concurrent invocations that can run at one time.\n - `env?` — `Map?` — Environment variables to pass to the function.\n - `logRetentionDays?` — `num?` — Specifies the number of days that function logs will be kept.\n - `memory?` — `num?` — The amount of memory to allocate to the function, in MB.\n - `timeout?` — `duration?` — The maximum amount of time the function can run.\n### Fields\n- `env` — `Map` — Returns the set of environment variables for this function.\n- `node` — `Node` — The tree node.\n### Methods\n- `addEnvironment` — `preflight (name: str, value: str): void` — Add an environment variable to the function.\n- `invoke` — `inflight (payload: str?): str?` — Invokes the function with a payload and waits for the result.\n- `invokeAsync` — `inflight (payload: str?): void` — Kicks off the execution of the function with a payload and returns immediately while the function is running.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `toString` — `preflight (): str` — Returns a string representation of this construct."
sortText: gg|Function
insertText: Function($1)
insertTextFormat: 2
@@ -71,7 +71,7 @@ source: libs/wingc/src/lsp/completions.rs
kind: 7
documentation:
kind: markdown
- value: "```wing\nclass OnDeploy\n```\n---\nRun code every time the app is deployed.\n\n### Initializer\n- `handler` — `inflight (): void`\n- `...props` — `OnDeployProps?`\n \n - `env?` — `Map?`\n - `executeAfter?` — `Array?` — Execute this trigger only after these resources have been provisioned.\n - `executeBefore?` — `Array?` — Adds this trigger as a dependency on other constructs.\n - `logRetentionDays?` — `num?`\n - `memory?` — `num?`\n - `timeout?` — `duration?`\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `toString` — `preflight (): str` — Returns a string representation of this construct."
+ value: "```wing\nclass OnDeploy\n```\n---\nRun code every time the app is deployed.\n\n### Initializer\n- `handler` — `inflight (): void`\n- `...props` — `OnDeployProps?`\n \n - `concurrency?` — `num?`\n - `env?` — `Map?`\n - `executeAfter?` — `Array?` — Execute this trigger only after these resources have been provisioned.\n - `executeBefore?` — `Array?` — Adds this trigger as a dependency on other constructs.\n - `logRetentionDays?` — `num?`\n - `memory?` — `num?`\n - `timeout?` — `duration?`\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `toString` — `preflight (): str` — Returns a string representation of this construct."
sortText: gg|OnDeploy
insertText: OnDeploy($1)
insertTextFormat: 2
diff --git a/libs/wingc/src/lsp/snapshots/completions/partial_type_reference_annotation.snap b/libs/wingc/src/lsp/snapshots/completions/partial_type_reference_annotation.snap
index a121951c269..bfe32f53fca 100644
--- a/libs/wingc/src/lsp/snapshots/completions/partial_type_reference_annotation.snap
+++ b/libs/wingc/src/lsp/snapshots/completions/partial_type_reference_annotation.snap
@@ -35,13 +35,13 @@ source: libs/wingc/src/lsp/completions.rs
kind: 7
documentation:
kind: markdown
- value: "```wing\nclass Function impl IInflightHost\n```\n---\nA function.\n\n### Initializer\n- `handler` — `inflight (event: str?): str?`\n- `...props` — `FunctionProps?`\n \n - `env?` — `Map?` — Environment variables to pass to the function.\n - `logRetentionDays?` — `num?` — Specifies the number of days that function logs will be kept.\n - `memory?` — `num?` — The amount of memory to allocate to the function, in MB.\n - `timeout?` — `duration?` — The maximum amount of time the function can run.\n### Fields\n- `env` — `Map` — Returns the set of environment variables for this function.\n- `node` — `Node` — The tree node.\n### Methods\n- `addEnvironment` — `preflight (name: str, value: str): void` — Add an environment variable to the function.\n- `invoke` — `inflight (payload: str?): str?` — Invokes the function with a payload and waits for the result.\n- `invokeAsync` — `inflight (payload: str?): void` — Kicks off the execution of the function with a payload and returns immediately while the function is running.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `toString` — `preflight (): str` — Returns a string representation of this construct."
+ value: "```wing\nclass Function impl IInflightHost\n```\n---\nA function.\n\n### Initializer\n- `handler` — `inflight (event: str?): str?`\n- `...props` — `FunctionProps?`\n \n - `concurrency?` — `num?` — The maximum concurrent invocations that can run at one time.\n - `env?` — `Map?` — Environment variables to pass to the function.\n - `logRetentionDays?` — `num?` — Specifies the number of days that function logs will be kept.\n - `memory?` — `num?` — The amount of memory to allocate to the function, in MB.\n - `timeout?` — `duration?` — The maximum amount of time the function can run.\n### Fields\n- `env` — `Map` — Returns the set of environment variables for this function.\n- `node` — `Node` — The tree node.\n### Methods\n- `addEnvironment` — `preflight (name: str, value: str): void` — Add an environment variable to the function.\n- `invoke` — `inflight (payload: str?): str?` — Invokes the function with a payload and waits for the result.\n- `invokeAsync` — `inflight (payload: str?): void` — Kicks off the execution of the function with a payload and returns immediately while the function is running.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `toString` — `preflight (): str` — Returns a string representation of this construct."
sortText: gg|Function
- label: OnDeploy
kind: 7
documentation:
kind: markdown
- value: "```wing\nclass OnDeploy\n```\n---\nRun code every time the app is deployed.\n\n### Initializer\n- `handler` — `inflight (): void`\n- `...props` — `OnDeployProps?`\n \n - `env?` — `Map?`\n - `executeAfter?` — `Array?` — Execute this trigger only after these resources have been provisioned.\n - `executeBefore?` — `Array?` — Adds this trigger as a dependency on other constructs.\n - `logRetentionDays?` — `num?`\n - `memory?` — `num?`\n - `timeout?` — `duration?`\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `toString` — `preflight (): str` — Returns a string representation of this construct."
+ value: "```wing\nclass OnDeploy\n```\n---\nRun code every time the app is deployed.\n\n### Initializer\n- `handler` — `inflight (): void`\n- `...props` — `OnDeployProps?`\n \n - `concurrency?` — `num?`\n - `env?` — `Map?`\n - `executeAfter?` — `Array?` — Execute this trigger only after these resources have been provisioned.\n - `executeBefore?` — `Array?` — Adds this trigger as a dependency on other constructs.\n - `logRetentionDays?` — `num?`\n - `memory?` — `num?`\n - `timeout?` — `duration?`\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `toString` — `preflight (): str` — Returns a string representation of this construct."
sortText: gg|OnDeploy
- label: Queue
kind: 7
@@ -245,7 +245,7 @@ source: libs/wingc/src/lsp/completions.rs
kind: 22
documentation:
kind: markdown
- value: "```wing\nstruct FunctionProps\n```\n---\nOptions for `Function`.\n### Fields\n- `env?` — `Map?` — Environment variables to pass to the function.\n- `logRetentionDays?` — `num?` — Specifies the number of days that function logs will be kept.\n- `memory?` — `num?` — The amount of memory to allocate to the function, in MB.\n- `timeout?` — `duration?` — The maximum amount of time the function can run."
+ value: "```wing\nstruct FunctionProps\n```\n---\nOptions for `Function`.\n### Fields\n- `concurrency?` — `num?` — The maximum concurrent invocations that can run at one time.\n- `env?` — `Map?` — Environment variables to pass to the function.\n- `logRetentionDays?` — `num?` — Specifies the number of days that function logs will be kept.\n- `memory?` — `num?` — The amount of memory to allocate to the function, in MB.\n- `timeout?` — `duration?` — The maximum amount of time the function can run."
sortText: hh|FunctionProps
- label: GetSecretValueOptions
kind: 22
@@ -263,7 +263,7 @@ source: libs/wingc/src/lsp/completions.rs
kind: 22
documentation:
kind: markdown
- value: "```wing\nstruct OnDeployProps extends FunctionProps\n```\n---\nOptions for `OnDeploy`.\n### Fields\n- `env?` — `Map?`\n- `executeAfter?` — `Array?` — Execute this trigger only after these resources have been provisioned.\n- `executeBefore?` — `Array?` — Adds this trigger as a dependency on other constructs.\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
+ value: "```wing\nstruct OnDeployProps extends FunctionProps\n```\n---\nOptions for `OnDeploy`.\n### Fields\n- `concurrency?` — `num?`\n- `env?` — `Map?`\n- `executeAfter?` — `Array?` — Execute this trigger only after these resources have been provisioned.\n- `executeBefore?` — `Array?` — Adds this trigger as a dependency on other constructs.\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
sortText: hh|OnDeployProps
- label: QueueProps
kind: 22
@@ -275,13 +275,13 @@ source: libs/wingc/src/lsp/completions.rs
kind: 22
documentation:
kind: markdown
- value: "```wing\nstruct QueueSetConsumerOptions extends FunctionProps\n```\n---\nOptions for Queue.setConsumer.\n### Fields\n- `batchSize?` — `num?` — The maximum number of messages to send to subscribers at once.\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
+ value: "```wing\nstruct QueueSetConsumerOptions extends FunctionProps\n```\n---\nOptions for Queue.setConsumer.\n### Fields\n- `batchSize?` — `num?` — The maximum number of messages to send to subscribers at once.\n- `concurrency?` — `num?`\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
sortText: hh|QueueSetConsumerOptions
- label: ScheduleOnTickOptions
kind: 22
documentation:
kind: markdown
- value: "```wing\nstruct ScheduleOnTickOptions extends FunctionProps\n```\n---\nOptions for Schedule.onTick.\n### Fields\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
+ value: "```wing\nstruct ScheduleOnTickOptions extends FunctionProps\n```\n---\nOptions for Schedule.onTick.\n### Fields\n- `concurrency?` — `num?`\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
sortText: hh|ScheduleOnTickOptions
- label: ScheduleProps
kind: 22
@@ -299,7 +299,7 @@ source: libs/wingc/src/lsp/completions.rs
kind: 22
documentation:
kind: markdown
- value: "```wing\nstruct ServiceOnStartOptions extends FunctionProps\n```\n---\nOptions for Service.onStart.\n### Fields\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
+ value: "```wing\nstruct ServiceOnStartOptions extends FunctionProps\n```\n---\nOptions for Service.onStart.\n### Fields\n- `concurrency?` — `num?`\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
sortText: hh|ServiceOnStartOptions
- label: ServiceProps
kind: 22
@@ -311,7 +311,7 @@ source: libs/wingc/src/lsp/completions.rs
kind: 22
documentation:
kind: markdown
- value: "```wing\nstruct TopicOnMessageOptions extends FunctionProps\n```\n---\nOptions for `Topic.onMessage`.\n### Fields\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
+ value: "```wing\nstruct TopicOnMessageOptions extends FunctionProps\n```\n---\nOptions for `Topic.onMessage`.\n### Fields\n- `concurrency?` — `num?`\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
sortText: hh|TopicOnMessageOptions
- label: TopicProps
kind: 22
diff --git a/libs/wingc/src/lsp/snapshots/completions/variable_type_annotation_namespace.snap b/libs/wingc/src/lsp/snapshots/completions/variable_type_annotation_namespace.snap
index a121951c269..bfe32f53fca 100644
--- a/libs/wingc/src/lsp/snapshots/completions/variable_type_annotation_namespace.snap
+++ b/libs/wingc/src/lsp/snapshots/completions/variable_type_annotation_namespace.snap
@@ -35,13 +35,13 @@ source: libs/wingc/src/lsp/completions.rs
kind: 7
documentation:
kind: markdown
- value: "```wing\nclass Function impl IInflightHost\n```\n---\nA function.\n\n### Initializer\n- `handler` — `inflight (event: str?): str?`\n- `...props` — `FunctionProps?`\n \n - `env?` — `Map?` — Environment variables to pass to the function.\n - `logRetentionDays?` — `num?` — Specifies the number of days that function logs will be kept.\n - `memory?` — `num?` — The amount of memory to allocate to the function, in MB.\n - `timeout?` — `duration?` — The maximum amount of time the function can run.\n### Fields\n- `env` — `Map` — Returns the set of environment variables for this function.\n- `node` — `Node` — The tree node.\n### Methods\n- `addEnvironment` — `preflight (name: str, value: str): void` — Add an environment variable to the function.\n- `invoke` — `inflight (payload: str?): str?` — Invokes the function with a payload and waits for the result.\n- `invokeAsync` — `inflight (payload: str?): void` — Kicks off the execution of the function with a payload and returns immediately while the function is running.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `toString` — `preflight (): str` — Returns a string representation of this construct."
+ value: "```wing\nclass Function impl IInflightHost\n```\n---\nA function.\n\n### Initializer\n- `handler` — `inflight (event: str?): str?`\n- `...props` — `FunctionProps?`\n \n - `concurrency?` — `num?` — The maximum concurrent invocations that can run at one time.\n - `env?` — `Map?` — Environment variables to pass to the function.\n - `logRetentionDays?` — `num?` — Specifies the number of days that function logs will be kept.\n - `memory?` — `num?` — The amount of memory to allocate to the function, in MB.\n - `timeout?` — `duration?` — The maximum amount of time the function can run.\n### Fields\n- `env` — `Map` — Returns the set of environment variables for this function.\n- `node` — `Node` — The tree node.\n### Methods\n- `addEnvironment` — `preflight (name: str, value: str): void` — Add an environment variable to the function.\n- `invoke` — `inflight (payload: str?): str?` — Invokes the function with a payload and waits for the result.\n- `invokeAsync` — `inflight (payload: str?): void` — Kicks off the execution of the function with a payload and returns immediately while the function is running.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `toString` — `preflight (): str` — Returns a string representation of this construct."
sortText: gg|Function
- label: OnDeploy
kind: 7
documentation:
kind: markdown
- value: "```wing\nclass OnDeploy\n```\n---\nRun code every time the app is deployed.\n\n### Initializer\n- `handler` — `inflight (): void`\n- `...props` — `OnDeployProps?`\n \n - `env?` — `Map?`\n - `executeAfter?` — `Array?` — Execute this trigger only after these resources have been provisioned.\n - `executeBefore?` — `Array?` — Adds this trigger as a dependency on other constructs.\n - `logRetentionDays?` — `num?`\n - `memory?` — `num?`\n - `timeout?` — `duration?`\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `toString` — `preflight (): str` — Returns a string representation of this construct."
+ value: "```wing\nclass OnDeploy\n```\n---\nRun code every time the app is deployed.\n\n### Initializer\n- `handler` — `inflight (): void`\n- `...props` — `OnDeployProps?`\n \n - `concurrency?` — `num?`\n - `env?` — `Map?`\n - `executeAfter?` — `Array?` — Execute this trigger only after these resources have been provisioned.\n - `executeBefore?` — `Array?` — Adds this trigger as a dependency on other constructs.\n - `logRetentionDays?` — `num?`\n - `memory?` — `num?`\n - `timeout?` — `duration?`\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `toString` — `preflight (): str` — Returns a string representation of this construct."
sortText: gg|OnDeploy
- label: Queue
kind: 7
@@ -245,7 +245,7 @@ source: libs/wingc/src/lsp/completions.rs
kind: 22
documentation:
kind: markdown
- value: "```wing\nstruct FunctionProps\n```\n---\nOptions for `Function`.\n### Fields\n- `env?` — `Map?` — Environment variables to pass to the function.\n- `logRetentionDays?` — `num?` — Specifies the number of days that function logs will be kept.\n- `memory?` — `num?` — The amount of memory to allocate to the function, in MB.\n- `timeout?` — `duration?` — The maximum amount of time the function can run."
+ value: "```wing\nstruct FunctionProps\n```\n---\nOptions for `Function`.\n### Fields\n- `concurrency?` — `num?` — The maximum concurrent invocations that can run at one time.\n- `env?` — `Map?` — Environment variables to pass to the function.\n- `logRetentionDays?` — `num?` — Specifies the number of days that function logs will be kept.\n- `memory?` — `num?` — The amount of memory to allocate to the function, in MB.\n- `timeout?` — `duration?` — The maximum amount of time the function can run."
sortText: hh|FunctionProps
- label: GetSecretValueOptions
kind: 22
@@ -263,7 +263,7 @@ source: libs/wingc/src/lsp/completions.rs
kind: 22
documentation:
kind: markdown
- value: "```wing\nstruct OnDeployProps extends FunctionProps\n```\n---\nOptions for `OnDeploy`.\n### Fields\n- `env?` — `Map?`\n- `executeAfter?` — `Array?` — Execute this trigger only after these resources have been provisioned.\n- `executeBefore?` — `Array?` — Adds this trigger as a dependency on other constructs.\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
+ value: "```wing\nstruct OnDeployProps extends FunctionProps\n```\n---\nOptions for `OnDeploy`.\n### Fields\n- `concurrency?` — `num?`\n- `env?` — `Map?`\n- `executeAfter?` — `Array?` — Execute this trigger only after these resources have been provisioned.\n- `executeBefore?` — `Array?` — Adds this trigger as a dependency on other constructs.\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
sortText: hh|OnDeployProps
- label: QueueProps
kind: 22
@@ -275,13 +275,13 @@ source: libs/wingc/src/lsp/completions.rs
kind: 22
documentation:
kind: markdown
- value: "```wing\nstruct QueueSetConsumerOptions extends FunctionProps\n```\n---\nOptions for Queue.setConsumer.\n### Fields\n- `batchSize?` — `num?` — The maximum number of messages to send to subscribers at once.\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
+ value: "```wing\nstruct QueueSetConsumerOptions extends FunctionProps\n```\n---\nOptions for Queue.setConsumer.\n### Fields\n- `batchSize?` — `num?` — The maximum number of messages to send to subscribers at once.\n- `concurrency?` — `num?`\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
sortText: hh|QueueSetConsumerOptions
- label: ScheduleOnTickOptions
kind: 22
documentation:
kind: markdown
- value: "```wing\nstruct ScheduleOnTickOptions extends FunctionProps\n```\n---\nOptions for Schedule.onTick.\n### Fields\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
+ value: "```wing\nstruct ScheduleOnTickOptions extends FunctionProps\n```\n---\nOptions for Schedule.onTick.\n### Fields\n- `concurrency?` — `num?`\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
sortText: hh|ScheduleOnTickOptions
- label: ScheduleProps
kind: 22
@@ -299,7 +299,7 @@ source: libs/wingc/src/lsp/completions.rs
kind: 22
documentation:
kind: markdown
- value: "```wing\nstruct ServiceOnStartOptions extends FunctionProps\n```\n---\nOptions for Service.onStart.\n### Fields\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
+ value: "```wing\nstruct ServiceOnStartOptions extends FunctionProps\n```\n---\nOptions for Service.onStart.\n### Fields\n- `concurrency?` — `num?`\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
sortText: hh|ServiceOnStartOptions
- label: ServiceProps
kind: 22
@@ -311,7 +311,7 @@ source: libs/wingc/src/lsp/completions.rs
kind: 22
documentation:
kind: markdown
- value: "```wing\nstruct TopicOnMessageOptions extends FunctionProps\n```\n---\nOptions for `Topic.onMessage`.\n### Fields\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
+ value: "```wing\nstruct TopicOnMessageOptions extends FunctionProps\n```\n---\nOptions for `Topic.onMessage`.\n### Fields\n- `concurrency?` — `num?`\n- `env?` — `Map?`\n- `logRetentionDays?` — `num?`\n- `memory?` — `num?`\n- `timeout?` — `duration?`"
sortText: hh|TopicOnMessageOptions
- label: TopicProps
kind: 22
diff --git a/libs/wingc/src/type_check.rs b/libs/wingc/src/type_check.rs
index 2852074448a..c274525dd1c 100644
--- a/libs/wingc/src/type_check.rs
+++ b/libs/wingc/src/type_check.rs
@@ -1127,6 +1127,17 @@ impl TypeRef {
matches!(**self, Type::Function(_))
}
+ pub fn is_enum(&self) -> bool {
+ matches!(**self, Type::Enum(_))
+ }
+
+ pub fn is_stringable(&self) -> bool {
+ matches!(
+ **self,
+ Type::String | Type::Number | Type::Boolean | Type::Json(_) | Type::MutJson | Type::Enum(_) | Type::Anything
+ )
+ }
+
/// If this is a function and its last argument is a struct, return that struct.
pub fn get_function_struct_arg(&self) -> Option<&Struct> {
if let Some(func) = self.maybe_unwrap_option().as_function_sig() {
@@ -1338,7 +1349,7 @@ pub struct Types {
/// A map from source paths to type information about that path
/// If it's a file, we save its symbol environment, and if it's a directory, we save a namespace that points to
/// all of the symbol environments of the files (or subdirectories) in that directory
- source_file_envs: IndexMap,
+ pub source_file_envs: IndexMap,
pub libraries: SymbolEnv,
numeric_idx: usize,
string_idx: usize,
@@ -1557,18 +1568,6 @@ impl Types {
self.get_typeref(self.mut_json_idx)
}
- pub fn stringables(&self) -> Vec {
- // TODO: This should be more complex and return all types that have some stringification facility
- // see: https://github.com/winglang/wing/issues/741
- vec![
- self.string(),
- self.number(),
- self.json(),
- self.mut_json(),
- self.anything(),
- ]
- }
-
pub fn add_namespace(&mut self, n: Namespace) -> NamespaceRef {
self.namespaces.push(Box::new(n));
self.get_namespaceref(self.namespaces.len() - 1)
@@ -2063,7 +2062,7 @@ impl<'a> TypeChecker<'a> {
if let InterpolatedStringPart::Expr(interpolated_expr) = part {
let (exp_type, p) = self.type_check_exp(interpolated_expr, env);
phase = combine_phases(phase, p);
- self.validate_type_in(exp_type, &self.types.stringables(), interpolated_expr);
+ self.validate_type_is_stringable(exp_type, interpolated_expr);
}
});
(self.types.string(), phase)
@@ -3220,6 +3219,28 @@ impl<'a> TypeChecker<'a> {
first_expected_type
}
+ pub fn validate_type_is_stringable(&mut self, actual_type: TypeRef, span: &impl Spanned) {
+ // If it's a type we can't resolve then we silently ignore it, assuming an error was already reported
+ if actual_type.is_unresolved() || actual_type.is_inferred() {
+ return;
+ }
+
+ if !actual_type.is_stringable() {
+ let message = format!("Expected type to be stringable, but got \"{actual_type}\" instead");
+
+ let hint = if actual_type.maybe_unwrap_option().is_stringable() {
+ format!(
+ "{} is an optional, try unwrapping it with 'x ?? \"nil\"' or 'x!'",
+ actual_type
+ )
+ } else {
+ "str, num, bool, json, and enums are stringable".to_string()
+ };
+
+ self.spanned_error_with_hints(span, message, vec![hint]);
+ }
+ }
+
pub fn type_check_file_or_dir(&mut self, source_path: &Utf8Path, scope: &Scope) {
CompilationContext::set(CompilationPhase::TypeChecking, &scope.span);
self.type_check_scope(scope);
diff --git a/libs/wingcompiler/src/compile.ts b/libs/wingcompiler/src/compile.ts
index 06984032cbb..26f0533f270 100644
--- a/libs/wingcompiler/src/compile.ts
+++ b/libs/wingcompiler/src/compile.ts
@@ -164,8 +164,7 @@ export async function compile(entrypoint: string, options: CompileOptions): Prom
if (!existsSync(synthDir)) {
await fs.mkdir(workDir, { recursive: true });
}
-
- let preflightEntrypoint = await compileForPreflight({
+ const compileForPreflightResult = await compileForPreflight({
entrypointFile,
workDir,
wingDir,
@@ -185,6 +184,7 @@ export async function compile(entrypoint: string, options: CompileOptions): Prom
WING_VALUES: options.value?.length == 0 ? undefined : options.value,
WING_VALUES_FILE: options.values ?? defaultValuesFile(),
WING_NODE_MODULES: wingNodeModules,
+ WING_IMPORTED_NAMESPACES: compileForPreflightResult.compilerOutput?.imported_namespaces.join(";"),
};
if (options.rootId) {
@@ -201,10 +201,8 @@ export async function compile(entrypoint: string, options: CompileOptions): Prom
delete preflightEnv.Path;
}
}
-
- await runPreflightCodeInWorkerThread(preflightEntrypoint, preflightEnv);
+ await runPreflightCodeInWorkerThread(compileForPreflightResult.preflightEntrypoint, preflightEnv);
}
-
return synthDir;
}
@@ -219,6 +217,13 @@ function isEntrypointFile(path: string) {
);
}
+interface CompileForPreflightResult {
+ readonly preflightEntrypoint: string;
+ readonly compilerOutput?: {
+ imported_namespaces: string[];
+ }
+}
+
async function compileForPreflight(props: {
entrypointFile: string;
workDir: string;
@@ -226,7 +231,7 @@ async function compileForPreflight(props: {
synthDir: string;
color?: boolean;
log?: (...args: any[]) => void;
-}) {
+}): Promise {
if (props.entrypointFile.endsWith(".ts")) {
const typescriptFramework = await import("@wingcloud/framework")
.then((m) => m.internal)
@@ -239,10 +244,12 @@ npm i @wingcloud/framework
`);
});
- return await typescriptFramework.compile({
- workDir: props.workDir,
- entrypoint: props.entrypointFile,
- });
+ return {
+ preflightEntrypoint: await typescriptFramework.compile({
+ workDir: props.workDir,
+ entrypoint: props.entrypointFile,
+ })
+ }
} else {
let env: Record = {
RUST_BACKTRACE: "full",
@@ -278,8 +285,10 @@ npm i @wingcloud/framework
)}`;
props.log?.(`invoking %s with: "%s"`, WINGC_COMPILE, arg);
let compileSuccess: boolean;
+ let compilerOutput: string | number = "";
try {
- compileSuccess = wingCompiler.invoke(wingc, WINGC_COMPILE, arg) !== 0;
+ compilerOutput = wingCompiler.invoke(wingc, WINGC_COMPILE, arg);
+ compileSuccess = compilerOutput !== 0;
} catch (error) {
// This is a bug in the compiler, indicate a compilation failure.
// The bug details should be part of the diagnostics handling below.
@@ -290,7 +299,12 @@ npm i @wingcloud/framework
throw new CompileError(errors);
}
- return join(props.workDir, WINGC_PREFLIGHT);
+ return {
+ preflightEntrypoint: join(props.workDir, WINGC_PREFLIGHT),
+ compilerOutput: JSON.parse(
+ compilerOutput as string
+ ),
+ }
}
}
diff --git a/libs/wingcompiler/src/wingc.ts b/libs/wingcompiler/src/wingc.ts
index 56fd5a64569..9f3cceecf94 100644
--- a/libs/wingcompiler/src/wingc.ts
+++ b/libs/wingcompiler/src/wingc.ts
@@ -282,7 +282,7 @@ export function invoke(
argMemoryBuffer.set(bytes);
const result = exports[func](argPointer, bytes.byteLength);
-
+
if (result === 0 || result === undefined || result === 0n) {
return 0;
} else {
diff --git a/libs/wingsdk/.projen/deps.json b/libs/wingsdk/.projen/deps.json
index 9fd500c9965..8776ce3074e 100644
--- a/libs/wingsdk/.projen/deps.json
+++ b/libs/wingsdk/.projen/deps.json
@@ -253,6 +253,11 @@
"name": "@types/aws-lambda",
"type": "bundled"
},
+ {
+ "name": "@winglang/wingtunnels",
+ "version": "workspace:^",
+ "type": "bundled"
+ },
{
"name": "ajv",
"type": "bundled"
diff --git a/libs/wingsdk/.projenrc.ts b/libs/wingsdk/.projenrc.ts
index 8c7642eab67..dd182e0d22b 100644
--- a/libs/wingsdk/.projenrc.ts
+++ b/libs/wingsdk/.projenrc.ts
@@ -88,6 +88,8 @@ const project = new cdk.JsiiProject({
// enhanced diagnostics
"stacktracey",
"ulid",
+ // tunnels
+ "@winglang/wingtunnels@workspace:^",
],
devDeps: [
`@cdktf/provider-aws@^19`, // only for testing Wing plugins
diff --git a/libs/wingsdk/package.json b/libs/wingsdk/package.json
index 4c21da8e180..8cdc839af21 100644
--- a/libs/wingsdk/package.json
+++ b/libs/wingsdk/package.json
@@ -96,6 +96,7 @@
"@smithy/util-stream": "2.0.17",
"@smithy/util-utf8": "2.0.0",
"@types/aws-lambda": "^8.10.119",
+ "@winglang/wingtunnels": "workspace:^",
"ajv": "^8.12.0",
"cdktf": "0.20.3",
"constructs": "^10.3",
@@ -136,6 +137,7 @@
"@smithy/util-stream",
"@smithy/util-utf8",
"@types/aws-lambda",
+ "@winglang/wingtunnels",
"ajv",
"cdktf",
"cron-parser",
diff --git a/libs/wingsdk/src/cloud/endpoint.ts b/libs/wingsdk/src/cloud/endpoint.ts
index 79af4fb447f..509be13915e 100644
--- a/libs/wingsdk/src/cloud/endpoint.ts
+++ b/libs/wingsdk/src/cloud/endpoint.ts
@@ -55,7 +55,6 @@ export class Endpoint extends Resource {
super(scope, id);
- Node.of(this).hidden = true;
Node.of(this).title = "Endpoint";
Node.of(this).description = props?.label ?? "A cloud endpoint";
diff --git a/libs/wingsdk/src/cloud/function.md b/libs/wingsdk/src/cloud/function.md
index 0d576669965..06cc35c0baf 100644
--- a/libs/wingsdk/src/cloud/function.md
+++ b/libs/wingsdk/src/cloud/function.md
@@ -56,12 +56,12 @@ new cloud.Function(inflight () => {
## Function container reuse
-Most cloud providers will opportunistically reuse the function's container in additional invocations. It is possible
-to leverage this behavior to cache objects across function executions using `inflight new` and inflight fields.
+Most cloud providers will opportunistically reuse the function's container in additional invocations.
+It is possible to leverage this behavior to cache objects across function executions using `inflight new` and inflight fields.
The following example reads the `bigdata.json` file once and reuses it every time `query()` is called.
-```js
+```ts playground
bring cloud;
let big = new cloud.Bucket();
@@ -95,6 +95,14 @@ new cloud.Function(inflight () => {
The sim implementation of `cloud.Function` runs the inflight code as a JavaScript function.
+By default, a maximum of 10 workers can be processing requests sent to a `cloud.Function` concurrently, but this number can be adjusted with the `concurrency` property:
+
+```ts playground
+new cloud.Function(inflight () => {
+ // ... code that shouldn't run concurrently ...
+}, concurrency: 1);
+```
+
### AWS (`tf-aws` and `awscdk`)
The AWS implementation of `cloud.Function` uses [AWS Lambda](https://aws.amazon.com/lambda/).
diff --git a/libs/wingsdk/src/cloud/function.ts b/libs/wingsdk/src/cloud/function.ts
index 95786aff4be..ffcc9d01fdb 100644
--- a/libs/wingsdk/src/cloud/function.ts
+++ b/libs/wingsdk/src/cloud/function.ts
@@ -40,6 +40,12 @@ export interface FunctionProps {
* @default 30
*/
readonly logRetentionDays?: number;
+
+ /**
+ * The maximum concurrent invocations that can run at one time.
+ * @default - platform specific limits (100 on the simulator)
+ */
+ readonly concurrency?: number;
}
/**
@@ -94,6 +100,12 @@ export class Function extends Resource implements IInflightHost {
if (process.env.WING_TARGET) {
this.addEnvironment("WING_TARGET", process.env.WING_TARGET);
}
+
+ if (props.concurrency !== undefined && props.concurrency <= 0) {
+ throw new Error(
+ "concurrency option on cloud.Function must be a positive integer"
+ );
+ }
}
/** @internal */
diff --git a/libs/wingsdk/src/core/tree.ts b/libs/wingsdk/src/core/tree.ts
index 61115da8abe..806b8895b37 100644
--- a/libs/wingsdk/src/core/tree.ts
+++ b/libs/wingsdk/src/core/tree.ts
@@ -4,6 +4,7 @@ import { IConstruct } from "constructs";
import { App } from "./app";
import { IResource, Node, Resource } from "../std";
import { VisualComponent } from "../ui/base";
+import { Colors, isOfTypeColors } from "../ui/colors";
export const TREE_FILE_PATH = "tree.json";
@@ -75,6 +76,11 @@ export interface DisplayInfo {
* @default - no UI components
*/
readonly ui?: any[]; // UIComponent
+
+ /**
+ * The color of the resource in the UI.
+ */
+ readonly color?: Colors;
}
/** @internal */
@@ -206,13 +212,20 @@ function synthDisplay(construct: IConstruct): DisplayInfo | undefined {
}
}
- if (display.description || display.title || display.hidden || ui) {
+ if (
+ display.description ||
+ display.title ||
+ display.hidden ||
+ ui ||
+ display.color
+ ) {
return {
title: display.title,
description: display.description,
hidden: display.hidden,
sourceModule: display.sourceModule,
ui: ui.length > 0 ? ui : undefined,
+ color: isOfTypeColors(display.color) ? display.color : undefined,
};
}
return;
diff --git a/libs/wingsdk/src/platform/platform-manager.ts b/libs/wingsdk/src/platform/platform-manager.ts
index da2a85b3ab7..321385ed188 100644
--- a/libs/wingsdk/src/platform/platform-manager.ts
+++ b/libs/wingsdk/src/platform/platform-manager.ts
@@ -3,6 +3,7 @@ import { basename, dirname, join } from "path";
import { cwd } from "process";
import * as vm from "vm";
import { IPlatform } from "./platform";
+import { scanDirForPlatformFile } from "./util";
import { App, AppProps, SynthHooks } from "../core";
interface PlatformManagerOptions {
@@ -21,6 +22,41 @@ export class PlatformManager {
constructor(options: PlatformManagerOptions) {
this.platformPaths = options.platformPaths ?? [];
+ this.retrieveImplicitPlatforms();
+ }
+
+ /**
+ * Retrieve all implicit platform declarations.
+ *
+ * These are platforms that are not explicitly declared in the cli options
+ * but are implicitly available to the app.
+ *
+ * We look for platforms in the following locations:
+ * - The source directory
+ * - Any imported namespaces (provided by the wingc compiler output)
+ *
+ * To determine if a directory contains a platform, we check if it contains a file ending in "wplatform.js"
+ * TODO: Support platforms defined in Wing (platform.w) https://github.com/winglang/wing/issues/4937
+ */
+ private retrieveImplicitPlatforms() {
+ const importedNamespaces = process.env.WING_IMPORTED_NAMESPACES?.split(";");
+ const sourceDir = process.env.WING_SOURCE_DIR!;
+
+ if (sourceDir) {
+ const sourceDirPlatformFile = scanDirForPlatformFile(sourceDir);
+ if (sourceDirPlatformFile) {
+ this.platformPaths.push(...sourceDirPlatformFile);
+ }
+ }
+
+ if (importedNamespaces) {
+ importedNamespaces.forEach((namespaceDir) => {
+ const namespaceDirPlatformFile = scanDirForPlatformFile(namespaceDir);
+ if (namespaceDirPlatformFile) {
+ this.platformPaths.push(...namespaceDirPlatformFile);
+ }
+ });
+ }
}
private loadPlatformPath(platformPath: string) {
diff --git a/libs/wingsdk/src/platform/util.ts b/libs/wingsdk/src/platform/util.ts
index b0220d1d8f7..7b92a6c5c66 100644
--- a/libs/wingsdk/src/platform/util.ts
+++ b/libs/wingsdk/src/platform/util.ts
@@ -1,4 +1,4 @@
-import { existsSync, readFileSync } from "fs";
+import { existsSync, readFileSync, readdirSync } from "fs";
import * as path from "path";
import * as toml from "toml";
import * as yaml from "yaml";
@@ -101,3 +101,26 @@ export function loadPlatformSpecificValues() {
})();
return { ...fileValues, ...cliValues };
}
+
+/**
+ * Scans a directory for any platform files.
+ *
+ * @param dir the directory to scan
+ * @returns the path to any platform files
+ */
+export function scanDirForPlatformFile(dir: string): string[] {
+ const result: string[] = [];
+
+ if (!existsSync(dir)) {
+ return result;
+ }
+
+ const files = readdirSync(dir);
+ for (const file of files) {
+ if (file === "wplatform.js" || file.endsWith(".wplatform.js")) {
+ result.push(path.join(dir, file));
+ }
+ }
+
+ return result;
+}
diff --git a/libs/wingsdk/src/shared/sandbox.ts b/libs/wingsdk/src/shared/sandbox.ts
index 9d065bfc91a..4d73c7ce1be 100644
--- a/libs/wingsdk/src/shared/sandbox.ts
+++ b/libs/wingsdk/src/shared/sandbox.ts
@@ -2,8 +2,8 @@ import * as cp from "child_process";
import { writeFileSync } from "fs";
import { mkdtemp, readFile, stat } from "fs/promises";
import { tmpdir } from "os";
-import * as path from "path";
-import { createBundle } from "./bundling";
+import path from "path";
+import { Bundle, createBundle } from "./bundling";
import { processStream } from "./stream-processor";
export interface SandboxOptions {
@@ -35,45 +35,23 @@ type ProcessResponse =
};
export class Sandbox {
- private createBundlePromise: Promise;
- private entrypoint: string;
- private readonly exitingChildren: Promise[] = [];
- private readonly timeouts: NodeJS.Timeout[] = [];
- private readonly options: SandboxOptions;
-
- constructor(entrypoint: string, options: SandboxOptions = {}) {
- this.entrypoint = entrypoint;
- this.options = options;
- this.createBundlePromise = this.createBundle();
- }
-
- public async cleanup() {
- await this.createBundlePromise;
- for (const timeout of this.timeouts) {
- clearTimeout(timeout);
- }
- // Make sure all child processes have exited before cleaning up the sandbox.
- for (const exitingChild of this.exitingChildren) {
- await exitingChild;
- }
- }
-
- private async createBundle() {
+ public static async createBundle(
+ entrypoint: string,
+ log?: (message: string) => void
+ ): Promise {
const workdir = await mkdtemp(path.join(tmpdir(), "wing-bundles-"));
- // wrap contents with a shim that handles the communication with the parent process
- // we insert this shim before bundling to ensure source maps are generated correctly
- let contents = (await readFile(this.entrypoint)).toString();
+ let contents = (await readFile(entrypoint)).toString();
// log a warning if contents includes __dirname or __filename
if (contents.includes("__dirname") || contents.includes("__filename")) {
- this.options.log?.(
- false,
- "warn",
+ log?.(
`Warning: __dirname and __filename cannot be used within bundled cloud functions. There may be unexpected behavior.`
);
}
+ // wrap contents with a shim that handles the communication with the parent process
+ // we insert this shim before bundling to ensure source maps are generated correctly
contents = `
"use strict";
${contents}
@@ -87,30 +65,81 @@ process.on("message", async (message) => {
}
});
`;
- const wrappedPath = this.entrypoint.replace(/\.js$/, ".sandbox.js");
+ const wrappedPath = entrypoint.replace(/\.js$/, ".sandbox.js");
writeFileSync(wrappedPath, contents); // async fsPromises.writeFile "flush" option is not available in Node 20
const bundle = createBundle(wrappedPath, [], workdir);
- this.entrypoint = bundle.entrypointPath;
if (process.env.DEBUG) {
- const bundleSize = (await stat(bundle.entrypointPath)).size;
- this.debugLog(`Bundled code (${bundleSize} bytes).`);
+ const fileStats = await stat(entrypoint);
+ log?.(`Bundled code (${fileStats.size} bytes).`);
}
+
+ return bundle;
}
- public async call(fn: string, ...args: any[]): Promise {
- // wait for the bundle to finish creation
- await this.createBundlePromise;
+ private readonly entrypoint: string;
+ private readonly options: SandboxOptions;
+
+ private child: cp.ChildProcess | undefined;
+ private onChildMessage: ((message: ProcessResponse) => void) | undefined;
+ private onChildError: ((error: Error) => void) | undefined;
+ private onChildExit:
+ | ((code: number | null, signal: NodeJS.Signals | null) => void)
+ | undefined;
+
+ private timeout: NodeJS.Timeout | undefined;
+
+ // Tracks whether the sandbox is available to process a new request
+ // When call() is called, it sets this to false, and when it's returning
+ // a response or error, it sets it back to true.
+ private available = true;
+
+ constructor(entrypoint: string, options: SandboxOptions = {}) {
+ this.entrypoint = entrypoint;
+ this.options = options;
+ }
- this.debugLog("Forking process to run bundled code.");
+ public async cleanup() {
+ if (this.timeout) {
+ clearTimeout(this.timeout);
+ }
+
+ if (this.child) {
+ this.child.kill("SIGTERM");
+ this.child = undefined;
+ this.available = true;
+ }
+ }
+
+ public isAvailable(): boolean {
+ return this.available;
+ }
+
+ public async initialize() {
+ this.debugLog("Initializing sandbox.");
+ const childEnv = this.options.env ?? {};
+ if (
+ process.env.NODE_OPTIONS?.includes("--inspect") ||
+ process.execArgv.some((a) => a.startsWith("--inspect"))
+ ) {
+ // We're exposing a debugger, let's attempt to ensure the child process automatically attaches
+ childEnv.NODE_OPTIONS =
+ (childEnv.NODE_OPTIONS ?? "") + (process.env.NODE_OPTIONS ?? "");
+
+ // VSCode's debugger adds some environment variables that we want to pass to the child process
+ for (const key in process.env) {
+ if (key.startsWith("VSCODE_")) {
+ childEnv[key] = process.env[key]!;
+ }
+ }
+ }
- // start a Node.js process that runs the bundled code
+ // start a Node.js process that runs the inflight code
// note: unlike the fork(2) POSIX system call, child_process.fork()
// does not clone the current process
- const child = cp.fork(this.entrypoint, [], {
- env: this.options.env,
+ this.child = cp.fork(this.entrypoint, {
+ env: childEnv,
stdio: "pipe",
- // execArgv: ["--enable-source-maps"],
// this option allows complex objects like Error to be sent from the child process to the parent
serialization: "advanced",
});
@@ -120,82 +149,97 @@ process.on("message", async (message) => {
this.options.log?.(false, "error", message);
// pipe stdout and stderr from the child process
- if (child.stdout) {
- processStream(child.stdout, log);
+ if (this.child.stdout) {
+ processStream(this.child.stdout, log);
}
- if (child.stderr) {
- processStream(child.stderr, logError);
+ if (this.child.stderr) {
+ processStream(this.child.stderr, logError);
}
- // Keep track of when the child process exits so that the simulator doesn't try
- // to clean up the sandbox before the child process has exited.
- // NOTE: If child processes are taking too long to exit (preventing simulator cleanups),
- // we could add a mechanism to send SIGKILL to the child process after a certain amount of time.
- let childExited: (value?: any) => void;
- this.exitingChildren.push(
- new Promise((resolve) => {
- childExited = resolve;
- })
- );
+ this.child.on("message", (message: ProcessResponse) => {
+ this.onChildMessage?.(message);
+ });
+ this.child!.on("error", (error) => {
+ this.onChildError?.(error);
+ });
+ this.child!.on("exit", (code, signal) => {
+ this.onChildExit?.(code, signal);
+ });
+ }
+
+ public async call(fn: string, ...args: any[]): Promise {
+ // Prevent multiple calls to the same sandbox running concurrently.
+ this.available = false;
+
+ // If this sandbox doesn't have a child process running (because it
+ // just got created, OR because the previous child process was killed due
+ // to timeout or an unexpected error), initialize one.
+ if (!this.child) {
+ await this.initialize();
+ }
// Send the function name and arguments to the child process.
- // When a message is received, kill the child process.
- // Once the child process is killed (by the parent process or because the user code
- // exited on its own), resolve or reject the promise.
+ // When a message is received, resolve or reject the promise.
return new Promise((resolve, reject) => {
- child.send({ fn, args } as ProcessRequest);
-
- let result: any;
- let status: "resolve" | "reject" | "pending" = "pending";
+ this.child!.send({ fn, args } as ProcessRequest);
- child.on("message", (message: ProcessResponse) => {
- this.debugLog("Received a message, killing child process.");
- child.kill();
+ this.onChildMessage = (message: ProcessResponse) => {
+ this.debugLog("Received a message from the sandbox.");
+ this.available = true;
+ if (this.timeout) {
+ clearTimeout(this.timeout);
+ }
if (message.type === "resolve") {
- result = message.value;
- status = "resolve";
+ resolve(message.value);
} else if (message.type === "reject") {
- result = message.reason;
- status = "reject";
+ reject(message.reason);
} else {
- result = `Parent received unexpected message from child process: ${message}`;
- status = "reject";
+ reject(
+ new Error(
+ `Unexpected message from sandbox child process: ${message}`
+ )
+ );
}
- });
+ };
// "error" could be emitted for any number of reasons
// (e.g. the process couldn't be spawned or killed, or a message couldn't be sent).
// Since this is unexpected, we kill the process with SIGKILL to ensure it's dead, and reject the promise.
- child.on("error", (error) => {
- this.debugLog("Killing process after error.");
- child.kill("SIGKILL");
- childExited();
- reject(`Unexpected error: ${error}`);
- });
-
- child.on("exit", (code, _signal) => {
+ this.onChildError = (error: Error) => {
+ this.debugLog("Unexpected error from the sandbox.");
+ this.child?.kill("SIGKILL");
+ this.child = undefined;
+ this.available = true;
+ if (this.timeout) {
+ clearTimeout(this.timeout);
+ }
+ reject(error);
+ };
+
+ // "exit" could be emitted if the user code called process.exit(), or if we killed the process
+ // due to a timeout or unexpected error. In any case, we reject the promise.
+ this.onChildExit = (code: number | null) => {
this.debugLog("Child processed stopped.");
- childExited();
- if (status === "pending") {
- // If the child process stopped without sending us back a message, reject the promise.
- reject(new Error(`Process exited with code ${code}`));
- } else if (status === "resolve") {
- resolve(result);
- } else if (status === "reject") {
- reject(result);
+ this.child = undefined;
+ this.available = true;
+ if (this.timeout) {
+ clearTimeout(this.timeout);
}
- });
+ reject(new Error(`Process exited with code ${code}`));
+ };
if (this.options.timeout) {
- const timeout = setTimeout(() => {
+ this.timeout = setTimeout(() => {
this.debugLog("Killing process after timeout.");
- child.kill();
- status = "reject";
- result = new Error(
- `Function timed out (it was configured to only run for ${this.options.timeout}ms)`
+ this.child?.kill("SIGTERM");
+ this.child = undefined;
+ this.available = true;
+ reject(
+ new Error(
+ `Function timed out (it was configured to only run for ${this.options.timeout}ms)`
+ )
);
}, this.options.timeout);
- this.timeouts.push(timeout);
}
});
}
diff --git a/libs/wingsdk/src/simulator/graph.ts b/libs/wingsdk/src/simulator/graph.ts
new file mode 100644
index 00000000000..c73a11b7118
--- /dev/null
+++ b/libs/wingsdk/src/simulator/graph.ts
@@ -0,0 +1,103 @@
+import { resolveTokens } from "./tokens";
+
+export interface Definition {
+ path: string;
+ deps?: string[];
+ props?: Record;
+}
+
+class Node {
+ public readonly dependencies = new Set();
+ public readonly dependents = new Set();
+ constructor(public readonly def: T) {}
+
+ public get path() {
+ return this.def.path;
+ }
+}
+
+export class Graph {
+ private byPath: Record> = {};
+
+ constructor(resources: T[] = []) {
+ for (const resource of resources) {
+ this.byPath[resource.path] = new Node(resource);
+ }
+
+ // build the dependency graph
+ for (const resource of resources) {
+ const consumer = resource.path;
+
+ // add explicit dependencies
+ for (const dep of resource.deps ?? []) {
+ this.recordDependency(consumer, dep);
+ }
+
+ // add implicit dependencies (e.g. from tokens in props)
+ const implicitDeps: string[] = [];
+
+ // collect all tokens from the props object (recursive) the "resolver" here is just a dummy
+ // function that collects all tokens and returns a dummy value (we don't care about the
+ // result).
+ resolveTokens(resource.props ?? {}, (token) => {
+ implicitDeps.push(token.path);
+ return "[T]"; // <-- we don't really use the result, just need to return something
+ });
+
+ // now add all implicit dependencies
+ for (const dep of implicitDeps) {
+ this.recordDependency(consumer, dep);
+ }
+ }
+ }
+
+ public get nodes(): Node[] {
+ return Object.values(this.byPath);
+ }
+
+ public tryFind(path: string): Node | undefined {
+ const node = this.byPath[path];
+ if (!node) {
+ return undefined;
+ }
+
+ return node;
+ }
+
+ private recordDependency(consumer: string, producer: string) {
+ this.tryFind(consumer)?.dependencies.add(producer);
+ this.tryFind(producer)?.dependents.add(consumer);
+
+ // check for cyclic dependencies
+ this.detectCycles(consumer);
+ this.detectCycles(producer);
+ }
+
+ private detectCycles(root: string) {
+ const visited = new Set();
+ const stack = new Set();
+
+ const visit = (path: string) => {
+ if (stack.has(path)) {
+ throw new Error(
+ `cyclic dependency detected: ${[...stack, path].join(" -> ")}`
+ );
+ }
+
+ if (visited.has(path)) {
+ return;
+ }
+
+ visited.add(path);
+ stack.add(path);
+
+ for (const dep of this.tryFind(path)?.dependencies ?? []) {
+ visit(dep);
+ }
+
+ stack.delete(path);
+ };
+
+ visit(root);
+ }
+}
diff --git a/libs/wingsdk/src/simulator/simulator.ts b/libs/wingsdk/src/simulator/simulator.ts
index 79f0e9c37dc..8a43e74d65a 100644
--- a/libs/wingsdk/src/simulator/simulator.ts
+++ b/libs/wingsdk/src/simulator/simulator.ts
@@ -3,19 +3,17 @@ import { mkdir, rm } from "fs/promises";
import type { Server, IncomingMessage, ServerResponse } from "http";
import { join } from "path";
import { makeSimulatorClient } from "./client";
+import { Graph } from "./graph";
import { deserialize, serialize } from "./serialization";
+import { resolveTokens } from "./tokens";
import { Tree } from "./tree";
import { SDK_VERSION } from "../constants";
-import { ConstructTree, TREE_FILE_PATH } from "../core";
+import { TREE_FILE_PATH } from "../core";
import { readJsonSync } from "../shared/misc";
import { CONNECTIONS_FILE_PATH, Trace, TraceType } from "../std";
-import {
- SIMULATOR_TOKEN_REGEX,
- SIMULATOR_TOKEN_REGEX_FULL,
-} from "../target-sim/tokens";
-const START_ATTEMPT_COUNT = 10;
const LOCALHOST_ADDRESS = "127.0.0.1";
+const HANDLE_ATTRIBUTE = "handle";
/**
* Props for `Simulator`.
@@ -160,13 +158,24 @@ export interface ITraceSubscriber {
*/
type RunningState = "starting" | "running" | "stopping" | "stopped";
+interface Model {
+ simdir: string;
+ tree: Tree;
+ connections: ConnectionData[];
+ schema: WingSimulatorSchema;
+ graph: Graph;
+}
+
+interface ResourceState {
+ props: Record;
+ attrs: Record;
+}
+
/**
* A simulator that can be used to test your application locally.
*/
export class Simulator {
// fields that are same between simulation runs / reloads
- private _config: WingSimulatorSchema;
- private readonly simdir: string;
private readonly statedir: string;
// fields that change between simulation runs / reloads
@@ -174,18 +183,18 @@ export class Simulator {
private readonly _handles: HandleManager;
private _traces: Array;
private readonly _traceSubscribers: Array;
- private _tree: Tree;
- private _connections: ConnectionData[];
private _serverUrl: string | undefined;
private _server: Server | undefined;
+ private _model: Model;
+
+ // keeps the actual resolved state (props and attrs) of all started resources. this state is
+ // merged in when calling `getResourceConfig()`.
+ private state: Record = {};
constructor(props: SimulatorProps) {
- this.simdir = props.simfile;
- this.statedir = props.stateDir ?? join(this.simdir, ".state");
- const { config, treeData, connectionData } = this._loadApp(props.simfile);
- this._config = config;
- this._tree = new Tree(treeData);
- this._connections = connectionData;
+ const simdir = props.simfile;
+ this.statedir = props.stateDir ?? join(simdir, ".state");
+ this._model = this._loadApp(simdir);
this._running = "stopped";
this._handles = new HandleManager();
@@ -193,50 +202,48 @@ export class Simulator {
this._traceSubscribers = new Array();
}
- private _loadApp(simdir: string): {
- config: any;
- treeData: ConstructTree;
- connectionData: ConnectionData[];
- } {
- const simJson = join(this.simdir, "simulator.json");
+ private _loadApp(simdir: string): Model {
+ const simJson = join(simdir, "simulator.json");
if (!existsSync(simJson)) {
throw new Error(
`Invalid Wing app (${simdir}) - simulator.json not found.`
);
}
- const config: WingSimulatorSchema = readJsonSync(simJson);
+ const schema = readJsonSync(simJson) as WingSimulatorSchema;
- const foundVersion = config.sdkVersion ?? "unknown";
+ const foundVersion = schema.sdkVersion ?? "unknown";
const expectedVersion = SDK_VERSION;
if (foundVersion !== expectedVersion) {
console.error(
`WARNING: The simulator directory (${simdir}) was generated with Wing SDK v${foundVersion} but it is being simulated with Wing SDK v${expectedVersion}.`
);
}
- if (config.resources === undefined) {
+ if (schema.resources === undefined) {
throw new Error(
`Incompatible .wsim file. The simulator directory (${simdir}) was generated with Wing SDK v${foundVersion} but it is being simulated with Wing SDK v${expectedVersion}.`
);
}
- const treeJson = join(this.simdir, TREE_FILE_PATH);
+ const treeJson = join(simdir, TREE_FILE_PATH);
if (!existsSync(treeJson)) {
throw new Error(
`Invalid Wing app (${simdir}) - ${TREE_FILE_PATH} not found.`
);
}
- const treeData = readJsonSync(treeJson);
- const connectionJson = join(this.simdir, CONNECTIONS_FILE_PATH);
+ const tree = new Tree(readJsonSync(treeJson));
+
+ const connectionJson = join(simdir, CONNECTIONS_FILE_PATH);
if (!existsSync(connectionJson)) {
throw new Error(
`Invalid Wing app (${simdir}) - ${CONNECTIONS_FILE_PATH} not found.`
);
}
- const connectionData = readJsonSync(connectionJson).connections;
+ const connections = readJsonSync(connectionJson).connections;
+ const graph = new Graph(schema.resources);
- return { config, treeData, connectionData };
+ return { schema, tree, connections, simdir, graph };
}
/**
@@ -250,45 +257,76 @@ export class Simulator {
}
this._running = "starting";
- // create a copy of the resource list to be used as an init queue.
- const initQueue: (BaseResourceSchema & { _attempts?: number })[] = [
- ...this._config.resources,
- ];
-
await this.startServer();
try {
- while (true) {
- const next = initQueue.shift();
- if (!next) {
- break;
- }
-
- // we couldn't start this resource yet, so decrement the retry counter and put it back in
- // the init queue.
- if (!(await this.tryStartResource(next))) {
- // we couldn't start this resource yet, so decrement the attempt counter
- next._attempts = next._attempts ?? START_ATTEMPT_COUNT;
- next._attempts--;
+ await this.startResources();
+ this._running = "running";
+ } catch (err) {
+ this.stopServer();
+ this._running = "stopped";
+ throw err;
+ }
+ }
- // if we've tried too many times, give up (might be a dependency cycle or a bad reference)
- if (next._attempts === 0) {
+ private async startResources() {
+ const retries: Record = {};
+ const queue = this._model.graph.nodes.map((n) => n.path);
+ while (queue.length > 0) {
+ const top = queue.shift()!;
+ try {
+ await this.startResource(top);
+ } catch (e) {
+ if (e instanceof UnresolvedTokenError) {
+ retries[top] = (retries[top] ?? 0) + 1;
+ if (retries[top] > 10) {
throw new Error(
- `Could not start resource ${next.path} after ${START_ATTEMPT_COUNT} attempts. This could be due to a dependency cycle or an invalid attribute reference.`
+ `Could not start resource after 10 attempts: ${e.message}`
);
}
-
- // put back in the queue for another round
- initQueue.push(next);
+ queue.push(top);
+ } else {
+ throw e;
}
}
+ }
+ }
- this._running = "running";
- } catch (err) {
- this.stopServer();
- this._running = "stopped";
- throw err;
+ /**
+ * Updates the running simulation with a new version of the app. This will create/update/delete
+ * resources as necessary to get to the desired state.
+ * @param simDir The path to the new version of the app
+ */
+ public async update(simDir: string) {
+ const newModel = this._loadApp(simDir);
+
+ const plan = planUpdate(
+ this._model.schema.resources,
+ newModel.schema.resources
+ );
+
+ this.addTrace({
+ type: TraceType.SIMULATOR,
+ data: {
+ message: `Update: ${plan.added.length} added, ${plan.updated.length} updated, ${plan.deleted.length} deleted`,
+ update: plan,
+ },
+ sourcePath: "root",
+ sourceType: "Simulator",
+ timestamp: new Date().toISOString(),
+ });
+
+ // stop all *deleted* and *updated* resources
+ for (const c of [...plan.deleted, ...plan.updated]) {
+ await this.stopResource(c); // <-- this also stops all dependent resources if needed
}
+
+ // now update the internal model to the new version
+ this._model = newModel;
+
+ // start all *added* and *updated* resources (the updated model basically includes only these)
+ // this will also start all dependencies as needed and not touch any resource that is already started
+ await this.startResources();
}
/**
@@ -308,31 +346,9 @@ export class Simulator {
}
this._running = "stopping";
- for (const resourceConfig of this._config.resources.slice().reverse()) {
- const handle = resourceConfig.attrs?.handle;
- if (!handle) {
- throw new Error(
- `Resource ${resourceConfig.path} could not be cleaned up, no handle for it was found.`
- );
- }
-
- try {
- const resource = this._handles.find(handle);
- await resource.save(this.getResourceStateDir(resourceConfig.path));
- this._handles.deallocate(handle);
- await resource.cleanup();
- } catch (err) {
- console.warn(err);
- }
-
- let event: Trace = {
- type: TraceType.RESOURCE,
- data: { message: `${resourceConfig.type} deleted.` },
- sourcePath: resourceConfig.path,
- sourceType: resourceConfig.type,
- timestamp: new Date().toISOString(),
- };
- this._addTrace(event);
+ // just call "stopResource" for all resources. it will stop all dependents as well.
+ for (const node of this._model.graph.nodes) {
+ await this.stopResource(node.path);
}
this.stopServer();
@@ -341,6 +357,51 @@ export class Simulator {
this._running = "stopped";
}
+ private isStarted(path: string): boolean {
+ return path in this.state;
+ }
+
+ private async stopResource(path: string) {
+ if (!this.isStarted(path)) {
+ return; // resource is already stopped
+ }
+
+ // first, stop all dependent resources
+ for (const consumer of this._model.graph.tryFind(path)?.dependents ?? []) {
+ await this.stopResource(consumer);
+ }
+
+ const handle = this.tryGetResourceHandle(path);
+ if (!handle) {
+ throw new Error(
+ `Resource ${path} could not be cleaned up, no handle for it was found.`
+ );
+ }
+
+ try {
+ const resource = this._handles.find(handle);
+ await resource.save(this.getResourceStateDir(path));
+ this._handles.deallocate(handle);
+ await resource.cleanup();
+ } catch (err) {
+ console.warn(err);
+ }
+
+ this.addSimulatorTrace(path, { message: `${path} stopped` });
+ delete this.state[path]; // delete the state of the resource
+ }
+
+ private addSimulatorTrace(path: string, data: any) {
+ const resourceConfig = this.getResourceConfig(path);
+ this.addTrace({
+ type: TraceType.SIMULATOR,
+ data: data,
+ sourcePath: resourceConfig.path,
+ sourceType: resourceConfig.type,
+ timestamp: new Date().toISOString(),
+ });
+ }
+
/**
* Stop the simulation, reload the simulation tree from the latest version of
* the app file, and restart the simulation.
@@ -352,10 +413,7 @@ export class Simulator {
await rm(this.statedir, { recursive: true });
}
- const { config, treeData, connectionData } = this._loadApp(this.simdir);
- this._config = config;
- this._tree = new Tree(treeData);
- this._connections = connectionData;
+ this._model = this._loadApp(this._model.simdir);
await this.start();
}
@@ -364,7 +422,7 @@ export class Simulator {
* Get a list of all resource paths.
*/
public listResources(): string[] {
- return this._config.resources.map((config) => config.path).sort();
+ return this._model.graph.nodes.map((x) => x.path).sort();
}
/**
@@ -391,7 +449,7 @@ export class Simulator {
* @returns The resource or undefined if not found
*/
public tryGetResource(path: string): any | undefined {
- const handle: string = this.tryGetResourceConfig(path)?.attrs.handle;
+ const handle = this.tryGetResourceHandle(path);
if (!handle) {
return undefined;
}
@@ -399,6 +457,10 @@ export class Simulator {
return makeSimulatorClient(this.url, handle);
}
+ private tryGetResourceHandle(path: string): string | undefined {
+ return this.tryGetResourceConfig(path)?.attrs[HANDLE_ATTRIBUTE];
+ }
+
/**
* Obtain a resource's configuration, including its type, props, and attrs.
* @returns The resource configuration or undefined if not found
@@ -408,7 +470,20 @@ export class Simulator {
if (path.startsWith("/")) {
path = `root${path}`;
}
- return this._config.resources.find((r) => r.path === path);
+
+ const def = this._model.graph.tryFind(path)?.def;
+ if (!def) {
+ return undefined;
+ }
+
+ const state = this.state[path];
+
+ return {
+ ...def,
+
+ // merge the actual state (props and attrs) over the desired state in `def`
+ ...state,
+ };
}
/**
@@ -447,7 +522,7 @@ export class Simulator {
}
private typeInfo(fqn: string): TypeSchema {
- return this._config.types[fqn];
+ return this._model.schema.types[fqn];
}
/**
@@ -462,14 +537,14 @@ export class Simulator {
* Obtain information about the application's construct tree.
*/
public tree(): Tree {
- return this._tree;
+ return this._model.tree;
}
/**
* Obtain information about the application's connections.
*/
public connections(): ConnectionData[] {
- return structuredClone(this._connections);
+ return structuredClone(this._model.connections);
}
/**
@@ -600,31 +675,20 @@ export class Simulator {
return this._serverUrl;
}
- private async tryStartResource(
- resourceConfig: BaseResourceSchema
- ): Promise {
- const context = this.createContext(resourceConfig);
-
- const { resolved, value: resolvedProps } = this.tryResolveTokens(
- resourceConfig.props
- );
- if (!resolved) {
- this._addTrace({
- type: TraceType.RESOURCE,
- data: { message: `${resourceConfig.path} is waiting on a dependency` },
- sourcePath: resourceConfig.path,
- sourceType: resourceConfig.type,
- timestamp: new Date().toISOString(),
- });
+ private async startResource(path: string): Promise {
+ if (this.isStarted(path)) {
+ return; // already started
+ }
- // this means the resource has a dependency that hasn't been started yet (hopefully). return
- // it to the init queue.
- return false;
+ // first lets make sure all my dependencies have been started (depth-first)
+ for (const d of this._model.graph.tryFind(path)?.dependencies ?? []) {
+ await this.startResource(d);
}
- // update the resource's config with the resolved props
- const config = this.getResourceConfig(resourceConfig.path);
- (config.props as any) = resolvedProps;
+ const resourceConfig = this.getResourceConfig(path);
+ const context = this.createContext(resourceConfig);
+
+ const resolvedProps = this.resolveTokens(resourceConfig.props);
// look up the location of the code for the type
const typeInfo = this.typeInfo(resourceConfig.type);
@@ -634,6 +698,12 @@ export class Simulator {
recursive: true,
});
+ // initialize the resource state object without attrs for now
+ this.state[path] = {
+ props: resolvedProps,
+ attrs: {},
+ };
+
// create the resource based on its type
// eslint-disable-next-line @typescript-eslint/no-require-imports
const ResourceType = require(typeInfo.sourcePath)[typeInfo.className];
@@ -646,24 +716,22 @@ export class Simulator {
// allocate a handle for the resource so others can find it
const handle = this._handles.allocate(resourceObject);
- // update the resource configuration with new attrs returned after initialization
- context.setResourceAttributes(resourceConfig.path, { ...attrs, handle });
+ // merge the attributes
+ this.state[path].attrs = {
+ ...this.state[path].attrs,
+ ...attrs,
+ [HANDLE_ATTRIBUTE]: handle,
+ };
// trace the resource creation
- this._addTrace({
- type: TraceType.RESOURCE,
- data: { message: `${resourceConfig.type} created.` },
- sourcePath: resourceConfig.path,
- sourceType: resourceConfig.type,
- timestamp: new Date().toISOString(),
+ this.addSimulatorTrace(path, {
+ message: `${resourceConfig.path} started`,
});
-
- return true;
}
private createContext(resourceConfig: BaseResourceSchema): ISimulatorContext {
return {
- simdir: this.simdir,
+ simdir: this._model.simdir,
statedir: join(this.statedir, resourceConfig.addr),
resourcePath: resourceConfig.path,
serverUrl: this.url,
@@ -671,13 +739,13 @@ export class Simulator {
return this._handles.find(handle);
},
addTrace: (trace: Trace) => {
- this._addTrace(trace);
+ this.addTrace(trace);
},
withTrace: async (props: IWithTraceProps) => {
// TODO: log start time and end time of activity?
try {
let result = await props.activity();
- this._addTrace({
+ this.addTrace({
data: {
message: props.message,
status: "success",
@@ -690,7 +758,7 @@ export class Simulator {
});
return result;
} catch (err) {
- this._addTrace({
+ this.addTrace({
data: { message: props.message, status: "failure", error: err },
type: TraceType.RESOURCE,
sourcePath: resourceConfig.path,
@@ -704,17 +772,21 @@ export class Simulator {
return [...this._traces];
},
setResourceAttributes: (path: string, attrs: Record) => {
- const config = this.getResourceConfig(path);
- const prev = config.attrs;
- (config as any).attrs = { ...prev, ...attrs };
+ for (const [key, value] of Object.entries(attrs)) {
+ this.addSimulatorTrace(path, {
+ message: `${path}.${key} = ${value}`,
+ });
+ }
+
+ this.state[path].attrs = { ...this.state[path].attrs, ...attrs };
},
resourceAttributes: (path: string) => {
- return this.getResourceConfig(path).attrs;
+ return this.state[path].attrs;
},
};
}
- private _addTrace(event: Trace) {
+ private addTrace(event: Trace) {
event = Object.freeze(event);
for (const sub of this._traceSubscribers) {
sub.callback(event);
@@ -722,40 +794,6 @@ export class Simulator {
this._traces.push(event);
}
- private tryResolveToken(s: string): { resolved: boolean; value: any } {
- const ref = s.slice(2, -1);
- const [_, path, rest] = ref.split("#");
- const config = this.getResourceConfig(path);
- if (rest.startsWith("attrs.")) {
- const attrName = rest.slice(6);
- const attr = config?.attrs[attrName];
-
- // we couldn't find the attribute. this doesn't mean it doesn't exist, it's just likely
- // that this resource haven't been started yet. so return `undefined`, which will cause
- // this resource to go back to the init queue.
- if (!attr) {
- return { resolved: false, value: undefined };
- }
- return { resolved: true, value: attr };
- } else if (rest.startsWith("props.")) {
- if (!config.props) {
- throw new Error(
- `Tried to resolve token "${s}" but resource ${path} has no props defined.`
- );
- }
- const propPath = rest.slice(6);
- const value = config.props[propPath];
- if (value === undefined) {
- throw new Error(
- `Tried to resolve token "${s}" but resource ${path} has no prop "${propPath}".`
- );
- }
- return { resolved: true, value };
- } else {
- throw new Error(`Invalid token reference: "${ref}"`);
- }
- }
-
/**
* Return an object with all tokens in it resolved to their appropriate values.
*
@@ -770,82 +808,38 @@ export class Simulator {
* @returns `undefined` if the token could not be resolved (e.g. needs a dependency), otherwise
* the resolved value.
*/
- private tryResolveTokens(obj: any): { resolved: boolean; value: any } {
- if (typeof obj === "string") {
- // there are two cases - a token can be the entire string, or it can be part of the string.
- // first, check if the entire string is a token
- if (SIMULATOR_TOKEN_REGEX_FULL.test(obj)) {
- const { resolved, value } = this.tryResolveToken(obj);
- if (!resolved) {
- return { resolved: false, value: undefined };
- }
- return { resolved: true, value };
- }
-
- // otherwise, check if the string contains tokens inside it. if so, we need to resolve them
- // and then check if the result is a string
- const globalRegex = new RegExp(SIMULATOR_TOKEN_REGEX.source, "g");
- const matches = obj.matchAll(globalRegex);
- const replacements = [];
- for (const match of matches) {
- const { resolved, value } = this.tryResolveToken(match[0]);
- if (!resolved) {
- return { resolved: false, value: undefined };
- }
- if (typeof value !== "string") {
- throw new Error(
- `Expected token "${
- match[0]
- }" to resolve to a string, but it resolved to ${typeof value}.`
- );
- }
- replacements.push({ match, value });
+ private resolveTokens(obj: any): any {
+ return resolveTokens(obj, (token) => {
+ const target = this._model.graph.tryFind(token.path);
+ if (!target) {
+ throw new Error(
+ `Could not resolve token "${token}" because the resource at path "${token.path}" does not exist.`
+ );
}
- // replace all the tokens in reverse order, and return the result
- // if a token returns another token (god forbid), do not resolve it again
- let result = obj;
- for (const { match, value } of replacements.reverse()) {
- if (match.index === undefined) {
- throw new Error(`unexpected error: match.index is undefined`);
- }
- result =
- result.slice(0, match.index) +
- value +
- result.slice(match.index + match[0].length);
- }
- return { resolved: true, value: result };
- }
+ const r = this.getResourceConfig(target.path);
- if (Array.isArray(obj)) {
- const result = [];
- for (const x of obj) {
- const { resolved, value } = this.tryResolveTokens(x);
- if (!resolved) {
- return { resolved: false, value: undefined };
+ if (token.attr) {
+ const value = r.attrs[token.attr];
+ if (value === undefined) {
+ throw new UnresolvedTokenError(
+ `Unable to resolve attribute '${token.attr}' for resource: ${target.path}`
+ );
}
- result.push(value);
+ return value;
}
- return { resolved: true, value: result };
- }
-
- if (typeof obj === "object") {
- const ret: any = {};
- for (const [key, v] of Object.entries(obj)) {
- const { resolved, value } = this.tryResolveTokens(v);
- if (!resolved) {
- return { resolved: false, value: undefined };
- }
- ret[key] = value;
+ if (token.prop) {
+ return r.props[token.prop];
}
- return { resolved: true, value: ret };
- }
- return { resolved: true, value: obj };
+ throw new Error(`Invalid token: ${token}`);
+ });
}
}
+class UnresolvedTokenError extends Error {}
+
/**
* A factory that can turn resource descriptions into (inflight) resource simulations.
*/
@@ -954,13 +948,14 @@ export interface BaseResourceSchema {
readonly props: { [key: string]: any };
/** The resource-specific attributes that are set after the resource is created. */
readonly attrs: Record;
- // TODO: model dependencies
+ /** Resources that should be deployed before this resource. */
+ readonly deps?: string[];
}
/** Schema for resource attributes */
export interface BaseResourceAttributes {
/** The resource's simulator-unique id. */
- readonly handle: string;
+ readonly [HANDLE_ATTRIBUTE]: string;
}
/** Schema for `.connections` in connections.json */
@@ -996,3 +991,66 @@ export interface SimulatorServerResponse {
/** The error that occurred during the method call. */
readonly error?: any;
}
+
+/**
+ * Given the "current" set of resources and a "next" set of resources, calculate the diff and
+ * determine which resources need to be added, updated or deleted.
+ *
+ * Note that dependencies are not considered here but they are implicitly handled by the
+ * `startResource` and `stopResource` methods. So, for example, when a resource is updated,
+ * all of it's dependents will be stopped and started again.
+ */
+function planUpdate(current: BaseResourceSchema[], next: BaseResourceSchema[]) {
+ const currentByPath = toMap(current);
+ const nextByPath = toMap(next);
+
+ const added: string[] = [];
+ const updated: string[] = [];
+ const deleted: string[] = [];
+
+ for (const [path, nextConfig] of Object.entries(nextByPath)) {
+ const currConfig = currentByPath[path];
+
+ // if the resource is not in "current", it means it was added
+ if (!currConfig) {
+ added.push(nextConfig.path);
+ continue;
+ }
+
+ // the resource is already in "current", if it's different from "next", it means it was updated
+ const state = (r: BaseResourceSchema) =>
+ JSON.stringify({
+ props: r.props,
+ type: r.type,
+ });
+
+ if (state(currConfig) !== state(nextConfig)) {
+ updated.push(nextConfig.path);
+ }
+
+ // remove it from "current" so we know what's left to be deleted
+ delete currentByPath[path];
+ }
+
+ // everything left in "current" is to be deleted
+ for (const config of Object.values(currentByPath)) {
+ deleted.push(config.path);
+ }
+
+ return { added, updated, deleted };
+
+ function toMap(list: BaseResourceSchema[]): {
+ [path: string]: BaseResourceSchema;
+ } {
+ const ret: { [path: string]: BaseResourceSchema } = {};
+ for (const resource of list) {
+ if (ret[resource.path]) {
+ throw new Error(
+ `unexpected - duplicate resources with the same path: ${resource.path}`
+ );
+ }
+ ret[resource.path] = resource;
+ }
+ return ret;
+ }
+}
diff --git a/libs/wingsdk/src/simulator/tokens.ts b/libs/wingsdk/src/simulator/tokens.ts
new file mode 100644
index 00000000000..b2ef9023e7f
--- /dev/null
+++ b/libs/wingsdk/src/simulator/tokens.ts
@@ -0,0 +1,113 @@
+import {
+ SIMULATOR_TOKEN_REGEX,
+ SIMULATOR_TOKEN_REGEX_FULL,
+} from "../target-sim/tokens";
+
+type Token = {
+ path: string;
+ attr?: string;
+ prop?: string;
+};
+
+export function parseToken(s: string): Token {
+ const ref = s.slice(2, -1);
+ const parts = ref.split("#");
+ if (parts.length !== 3) {
+ throw new Error(`Invalid token reference: ${s}`);
+ }
+
+ const [_, path, rest] = parts;
+
+ if (rest.startsWith("attrs.")) {
+ const attrName = rest.slice(6);
+ return { path, attr: attrName };
+ } else if (rest.startsWith("props.")) {
+ const propPath = rest.slice(6);
+ return { path, prop: propPath };
+ } else {
+ throw new Error(`Invalid token reference: ${s}`);
+ }
+}
+
+type TokenResolver = (token: Token) => string;
+
+/**
+ * Return an object with all tokens in it resolved to their appropriate values.
+ *
+ * A token can be a string like "${app/my_bucket#attrs.handle}". This token would be resolved to
+ * the "handle" attribute of the resource at path "app/my_bucket". If that attribute does not
+ * exist at the time of resolution (for example, if my_bucket is not being simulated yet), an
+ * error will be thrown.
+ *
+ * Tokens can also be nested, like "${app/my_bucket#attrs.handle}/foo/bar".
+ *
+ * @param obj The object to resolve tokens in.
+ * @returns The resolved token or throws an error if the token cannot be resolved.
+ */
+export function resolveTokens(obj: any, resolver: TokenResolver): any {
+ if (obj === undefined) {
+ return obj;
+ }
+
+ if (typeof obj === "string") {
+ // there are two cases - a token can be the entire string, or it can be part of the string.
+ // first, check if the entire string is a token
+ if (SIMULATOR_TOKEN_REGEX_FULL.test(obj)) {
+ return resolver(parseToken(obj));
+ }
+
+ // otherwise, check if the string contains tokens inside it. if so, we need to resolve them
+ // and then check if the result is a string
+ const globalRegex = new RegExp(SIMULATOR_TOKEN_REGEX.source, "g");
+ const matches = obj.matchAll(globalRegex);
+ const replacements = [];
+ for (const match of matches) {
+ const value = resolveTokens(match[0], resolver);
+
+ if (typeof value !== "string") {
+ throw new Error(
+ `Expected token "${
+ match[0]
+ }" to resolve to a string, but it resolved to ${typeof value}.`
+ );
+ }
+
+ replacements.push({ match, value });
+ }
+
+ // replace all the tokens in reverse order, and return the result
+ // if a token returns another token (god forbid), do not resolve it again
+ let result = obj;
+ for (const { match, value } of replacements.reverse()) {
+ if (match.index === undefined) {
+ throw new Error(`unexpected error: match.index is undefined`);
+ }
+ result =
+ result.slice(0, match.index) +
+ value +
+ result.slice(match.index + match[0].length);
+ }
+
+ return result;
+ }
+
+ if (Array.isArray(obj)) {
+ const result = [];
+ for (const x of obj) {
+ const value = resolveTokens(x, resolver);
+ result.push(value);
+ }
+
+ return result;
+ }
+
+ if (typeof obj === "object") {
+ const ret: any = {};
+ for (const [key, v] of Object.entries(obj)) {
+ ret[key] = resolveTokens(v, resolver);
+ }
+ return ret;
+ }
+
+ return obj;
+}
diff --git a/libs/wingsdk/src/std/node.ts b/libs/wingsdk/src/std/node.ts
index 43aefd94941..f3154e2a27f 100644
--- a/libs/wingsdk/src/std/node.ts
+++ b/libs/wingsdk/src/std/node.ts
@@ -62,6 +62,23 @@ export class Node {
*/
public hidden?: boolean;
+ /**
+ * The color of the construct for display purposes.
+ * Supported colors are:
+ * - orange
+ * - sky
+ * - emerald
+ * - lime
+ * - pink
+ * - amber
+ * - cyan
+ * - purple
+ * - red
+ * - violet
+ * - slate
+ */
+ public color?: string;
+
private readonly _constructsNode: ConstructsNode;
private readonly _connections: Connections;
private _app: IApp | undefined;
diff --git a/libs/wingsdk/src/std/test-runner.ts b/libs/wingsdk/src/std/test-runner.ts
index cf7c9a0f8b2..2b78ef799eb 100644
--- a/libs/wingsdk/src/std/test-runner.ts
+++ b/libs/wingsdk/src/std/test-runner.ts
@@ -232,6 +232,10 @@ export interface Trace {
* @skipDocs
*/
export enum TraceType {
+ /**
+ * A trace representing simulator activity.
+ */
+ SIMULATOR = "simulator",
/**
* A trace representing a resource activity.
*/
diff --git a/libs/wingsdk/src/target-sim/api.inflight.ts b/libs/wingsdk/src/target-sim/api.inflight.ts
index 5993cde0ff6..d8e43c575dd 100644
--- a/libs/wingsdk/src/target-sim/api.inflight.ts
+++ b/libs/wingsdk/src/target-sim/api.inflight.ts
@@ -56,7 +56,6 @@ export class Api
private port: number | undefined;
constructor(props: ApiSchema["props"], context: ISimulatorContext) {
- props;
this.routes = [];
this.context = context;
const { corsHeaders } = props;
@@ -131,8 +130,16 @@ export class Api
public async cleanup(): Promise {
this.addTrace(`Closing server on ${this.url}`);
- this.server?.close();
- this.server?.closeAllConnections();
+ return new Promise((resolve, reject) => {
+ this.server?.close((err) => {
+ if (err) {
+ return reject(err);
+ }
+
+ this.server?.closeAllConnections();
+ return resolve();
+ });
+ });
}
public async save(): Promise {
diff --git a/libs/wingsdk/src/target-sim/app.ts b/libs/wingsdk/src/target-sim/app.ts
index 11c4fe55aed..5d7b5de4ac4 100644
--- a/libs/wingsdk/src/target-sim/app.ts
+++ b/libs/wingsdk/src/target-sim/app.ts
@@ -12,7 +12,7 @@ import { OnDeploy } from "./on-deploy";
import { Queue } from "./queue";
import { ReactApp } from "./react-app";
import { Redis } from "./redis";
-import { isSimulatorResource } from "./resource";
+import { ISimulatorResource, isSimulatorResource } from "./resource";
import { Schedule } from "./schedule";
import { Secret } from "./secret";
import { Service } from "./service";
@@ -261,10 +261,22 @@ export class App extends core.App {
}
private synthSimulatorFile(outdir: string) {
+ const toSimulatorWithDeps = (res: ISimulatorResource) => {
+ const cfg = res.toSimulator();
+ const deps = res.node.dependencies.map((d) => d.node.path);
+
+ return deps.length === 0
+ ? cfg
+ : {
+ ...cfg,
+ deps,
+ };
+ };
+
const resources = new core.DependencyGraph(this.node)
.topology()
.filter(isSimulatorResource)
- .map((res) => res.toSimulator());
+ .map(toSimulatorWithDeps);
const types: { [fqn: string]: TypeSchema } = {};
for (const [fqn, className] of Object.entries(SIMULATOR_CLASS_DATA)) {
diff --git a/libs/wingsdk/src/target-sim/dynamodb-table.inflight.ts b/libs/wingsdk/src/target-sim/dynamodb-table.inflight.ts
index c11f70834b5..c28e40653a3 100644
--- a/libs/wingsdk/src/target-sim/dynamodb-table.inflight.ts
+++ b/libs/wingsdk/src/target-sim/dynamodb-table.inflight.ts
@@ -30,6 +30,7 @@ export class DynamodbTable
process.env.WING_DYNAMODB_IMAGE ?? "amazon/dynamodb-local:2.0.0";
private readonly context: ISimulatorContext;
private client?: DynamoDBClient;
+ private _endpoint?: string;
public constructor(
private props: DynamodbTableSchema["props"],
@@ -51,6 +52,8 @@ export class DynamodbTable
containerPort: "8000",
});
+ this._endpoint = `http://0.0.0.0:${hostPort}`;
+
// dynamodb url based on host port
this.client = new DynamoDBClient({
region: "local",
@@ -58,7 +61,7 @@ export class DynamodbTable
accessKeyId: "x",
secretAccessKey: "y",
},
- endpoint: `http://0.0.0.0:${hostPort}`,
+ endpoint: this._endpoint,
});
await this.createTable();
@@ -75,10 +78,22 @@ export class DynamodbTable
this.client?.destroy();
// stop the dynamodb container
await runCommand("docker", ["rm", "-f", this.containerName]);
+ this._endpoint = undefined;
}
public async save(): Promise {}
+ /**
+ * Returns the local endpoint of the DynamoDB table.
+ */
+ public async endpoint(): Promise {
+ if (!this._endpoint) {
+ throw new Error("DynamoDB hasn't been started");
+ }
+
+ return this._endpoint;
+ }
+
public async _rawClient(): Promise {
if (this.client) {
return this.client;
diff --git a/libs/wingsdk/src/target-sim/endpoint.inflight.ts b/libs/wingsdk/src/target-sim/endpoint.inflight.ts
index 5e8fd8cbe83..d78b77c81a6 100644
--- a/libs/wingsdk/src/target-sim/endpoint.inflight.ts
+++ b/libs/wingsdk/src/target-sim/endpoint.inflight.ts
@@ -1,22 +1,116 @@
+import { readFile, writeFile } from "node:fs/promises";
+import { join } from "node:path";
+import { connect, ConnectResponse } from "@winglang/wingtunnels";
import { EndpointAttributes, EndpointSchema } from "./schema-resources";
+import { exists } from "./util";
import { IEndpointClient } from "../cloud";
import { ISimulatorContext, ISimulatorResourceInstance } from "../simulator";
+const STATE_FILENAME = "state.json";
+
+/**
+ * Contents of the state file for this resource.
+ */
+interface StateFileContents {
+ /**
+ * The last subdomain used by the tunnel on a previous simulator run.
+ */
+ readonly subdomain?: string;
+}
+
+export type EndpointExposeStatus = "connected" | "disconnected" | "connecting";
+
export class Endpoint implements IEndpointClient, ISimulatorResourceInstance {
+ private connectResponse?: ConnectResponse;
+ private lastSubdomain?: string;
+ private status: EndpointExposeStatus = "disconnected";
constructor(
private readonly _props: EndpointSchema["props"],
- _context: ISimulatorContext
+ private readonly _context: ISimulatorContext
) {}
public async init(): Promise {
+ const state: StateFileContents = await this.loadState();
+ if (state.subdomain) {
+ await this.connect(state.subdomain);
+ }
+
return {
inputUrl: this._props.inputUrl,
- url: this._props.url,
+ url: this.connectResponse?.url ?? this._props.url,
label: this._props.label,
browserSupport: this._props.browserSupport,
};
}
- public async cleanup(): Promise {}
+ public async cleanup(): Promise {
+ this.connectResponse?.close();
+ }
+
+ public async save(): Promise {
+ return this.saveState({
+ ...(this.lastSubdomain && { subdomain: this.lastSubdomain }),
+ });
+ }
+
+ public async expose(): Promise {
+ if (this.status === "connecting" || this.status === "connected") {
+ throw new Error("Can only expose when status is disconnected.");
+ }
+ return this.connect();
+ }
- public async save(): Promise {}
+ public async hide(): Promise {
+ this.connectResponse?.close();
+ this.connectResponse = undefined;
+ this.lastSubdomain = undefined;
+ this.status = "disconnected";
+ }
+
+ public async exposeStatus(): Promise {
+ return this.status;
+ }
+
+ private async loadState(): Promise {
+ const stateFileExists = await exists(
+ join(this._context.statedir, STATE_FILENAME)
+ );
+ if (stateFileExists) {
+ const stateFileContents = await readFile(
+ join(this._context.statedir, STATE_FILENAME),
+ "utf-8"
+ );
+ return JSON.parse(stateFileContents);
+ } else {
+ return {};
+ }
+ }
+
+ private async saveState(state: StateFileContents): Promise {
+ await writeFile(
+ join(this._context.statedir, STATE_FILENAME),
+ JSON.stringify(state)
+ );
+ }
+
+ private async connect(subdomain?: string) {
+ try {
+ await this._context.withTrace({
+ message: `Creating tunnel for endpoint. ${
+ subdomain ? `Using subdomain: ${subdomain}` : ""
+ }`,
+ activity: async () => {
+ this.status = "connecting";
+ this.connectResponse = await connect(this._props.inputUrl, {
+ subdomain,
+ });
+ this.lastSubdomain = new URL(this.connectResponse.url).hostname.split(
+ "."
+ )[0];
+ this.status = "connected";
+ },
+ });
+ } catch {
+ this.status = "disconnected";
+ }
+ }
}
diff --git a/libs/wingsdk/src/target-sim/function.inflight.ts b/libs/wingsdk/src/target-sim/function.inflight.ts
index bc5918b49e2..cbb1b93be5c 100644
--- a/libs/wingsdk/src/target-sim/function.inflight.ts
+++ b/libs/wingsdk/src/target-sim/function.inflight.ts
@@ -1,6 +1,7 @@
import * as path from "path";
import { FunctionAttributes, FunctionSchema } from "./schema-resources";
import { FUNCTION_FQN, IFunctionClient } from "../cloud";
+import { Bundle } from "../shared/bundling";
import { Sandbox } from "../shared/sandbox";
import {
ISimulatorContext,
@@ -9,36 +10,26 @@ import {
import { TraceType } from "../std";
export class Function implements IFunctionClient, ISimulatorResourceInstance {
- private readonly filename: string;
+ private readonly originalFile: string;
+ private bundle: Bundle | undefined;
private readonly env: Record;
private readonly context: ISimulatorContext;
private readonly timeout: number;
- private readonly sandbox: Sandbox;
+ private readonly maxWorkers: number;
+ private readonly workers = new Array();
+ private createBundlePromise: Promise;
constructor(props: FunctionSchema["props"], context: ISimulatorContext) {
if (props.sourceCodeLanguage !== "javascript") {
throw new Error("Only JavaScript is supported");
}
- this.filename = path.resolve(context.simdir, props.sourceCodeFile);
+ this.originalFile = path.resolve(context.simdir, props.sourceCodeFile);
this.env = props.environmentVariables ?? {};
this.context = context;
this.timeout = props.timeout;
- this.sandbox = new Sandbox(this.filename, {
- env: {
- ...this.env,
- WING_SIMULATOR_URL: this.context.serverUrl,
- },
- timeout: this.timeout,
- log: (internal, _level, message) => {
- this.context.addTrace({
- data: { message },
- type: internal ? TraceType.RESOURCE : TraceType.LOG,
- sourcePath: this.context.resourcePath,
- sourceType: FUNCTION_FQN,
- timestamp: new Date().toISOString(),
- });
- },
- });
+ this.maxWorkers = props.concurrency;
+
+ this.createBundlePromise = this.createBundle();
}
public async init(): Promise {
@@ -46,7 +37,15 @@ export class Function implements IFunctionClient, ISimulatorResourceInstance {
}
public async cleanup(): Promise {
- await this.sandbox.cleanup();
+ // We wait for the bundle to be created since there's no way to otherwise cancel the work.
+ // If the simulator runs for a short time (and cloud.Function is created and then deleted)
+ // and the bundling code is allowed to run after the simulator has stopped, it might fail
+ // and throw an error to the user because the files the simulator was using may no longer be there there.
+ await this.createBundlePromise;
+
+ for (const worker of this.workers) {
+ await worker.cleanup();
+ }
}
public async save(): Promise {}
@@ -54,7 +53,15 @@ export class Function implements IFunctionClient, ISimulatorResourceInstance {
public async invoke(payload: string): Promise {
return this.context.withTrace({
message: `Invoke (payload=${JSON.stringify(payload)}).`,
- activity: () => this.sandbox.call("handler", payload),
+ activity: async () => {
+ const worker = await this.findAvailableWorker();
+ if (!worker) {
+ throw new Error(
+ "Too many requests, the function has reached its concurrency limit."
+ );
+ }
+ return worker.call("handler", payload);
+ },
});
}
@@ -62,10 +69,89 @@ export class Function implements IFunctionClient, ISimulatorResourceInstance {
await this.context.withTrace({
message: `InvokeAsync (payload=${JSON.stringify(payload)}).`,
activity: async () => {
+ const worker = await this.findAvailableWorker();
+ if (!worker) {
+ throw new Error(
+ "Too many requests, the function has reached its concurrency limit."
+ );
+ }
process.nextTick(() => {
- void this.sandbox.call("handler", payload);
+ // If the call fails, we log the error and continue since we've already
+ // handed control back to the caller.
+ void worker.call("handler", payload).catch((e) => {
+ this.context.addTrace({
+ data: {
+ message: `InvokeAsync (payload=${JSON.stringify(payload)}).`,
+ status: "failure",
+ error: e,
+ },
+ type: TraceType.RESOURCE,
+ sourcePath: this.context.resourcePath,
+ sourceType: FUNCTION_FQN,
+ timestamp: new Date().toISOString(),
+ });
+ });
});
},
});
}
+
+ private async createBundle(): Promise {
+ this.bundle = await Sandbox.createBundle(this.originalFile, (msg) => {
+ this.addTrace(msg);
+ });
+ }
+
+ // Used internally by cloud.Queue to apply backpressure
+ public async hasAvailableWorkers(): Promise {
+ return (
+ this.workers.length < this.maxWorkers ||
+ this.workers.some((w) => w.isAvailable())
+ );
+ }
+
+ private async findAvailableWorker(): Promise {
+ const worker = this.workers.find((w) => w.isAvailable());
+ if (worker) {
+ return worker;
+ }
+
+ if (this.workers.length < this.maxWorkers) {
+ const newWorker = await this.initWorker();
+ this.workers.push(newWorker);
+ return newWorker;
+ }
+
+ return undefined;
+ }
+
+ private async initWorker(): Promise {
+ // ensure inflight code is bundled before we create any workers
+ await this.createBundlePromise;
+
+ if (!this.bundle) {
+ throw new Error("Bundle not created");
+ }
+
+ return new Sandbox(this.bundle.entrypointPath, {
+ env: {
+ ...this.env,
+ WING_SIMULATOR_URL: this.context.serverUrl,
+ },
+ timeout: this.timeout,
+ log: (internal, _level, message) => {
+ this.addTrace(message, internal);
+ },
+ });
+ }
+
+ private addTrace(message: string, internal: boolean = true) {
+ this.context.addTrace({
+ data: { message },
+ type: internal ? TraceType.RESOURCE : TraceType.LOG,
+ sourcePath: this.context.resourcePath,
+ sourceType: FUNCTION_FQN,
+ timestamp: new Date().toISOString(),
+ });
+ }
}
diff --git a/libs/wingsdk/src/target-sim/function.ts b/libs/wingsdk/src/target-sim/function.ts
index a711bbce017..3a77d1d7951 100644
--- a/libs/wingsdk/src/target-sim/function.ts
+++ b/libs/wingsdk/src/target-sim/function.ts
@@ -21,6 +21,7 @@ export const ENV_WING_SIM_INFLIGHT_RESOURCE_TYPE =
*/
export class Function extends cloud.Function implements ISimulatorResource {
private readonly timeout: Duration;
+ private readonly concurrency: number;
constructor(
scope: Construct,
id: string,
@@ -31,6 +32,7 @@ export class Function extends cloud.Function implements ISimulatorResource {
// props.memory is unused since we are not simulating it
this.timeout = props.timeout ?? Duration.fromMinutes(1);
+ this.concurrency = props.concurrency ?? 100;
}
public toSimulator(): BaseResourceSchema {
@@ -44,6 +46,7 @@ export class Function extends cloud.Function implements ISimulatorResource {
sourceCodeLanguage: "javascript",
environmentVariables: this.env,
timeout: this.timeout.seconds * 1000,
+ concurrency: this.concurrency,
},
attrs: {} as any,
};
diff --git a/libs/wingsdk/src/target-sim/queue.inflight.ts b/libs/wingsdk/src/target-sim/queue.inflight.ts
index dbf71f7744a..9d5ca729034 100644
--- a/libs/wingsdk/src/target-sim/queue.inflight.ts
+++ b/libs/wingsdk/src/target-sim/queue.inflight.ts
@@ -1,4 +1,5 @@
import { IEventPublisher } from "./event-mapping";
+import type { Function as FunctionClient } from "./function.inflight";
import {
QueueAttributes,
QueueSchema,
@@ -149,6 +150,16 @@ export class Queue
if (!fnClient) {
throw new Error("No function client found");
}
+
+ // If the function we picked is at capacity, keep the messages in the queue
+ const hasWorkers = await (
+ fnClient as FunctionClient
+ ).hasAvailableWorkers();
+ if (!hasWorkers) {
+ this.messages.push(...messages);
+ continue;
+ }
+
this.context.addTrace({
type: TraceType.RESOURCE,
data: {
@@ -166,6 +177,14 @@ export class Queue
void fnClient
.invoke(JSON.stringify({ messages: messagesPayload }))
.catch((err) => {
+ // If the function is at a concurrency limit, pretend we just didn't call it
+ if (
+ err.message ===
+ "Too many requests, the function has reached its concurrency limit."
+ ) {
+ this.messages.push(...messages);
+ return;
+ }
// If the function returns an error, put the message back on the queue after timeout period
this.context.addTrace({
data: {
diff --git a/libs/wingsdk/src/target-sim/react-app.inflight.ts b/libs/wingsdk/src/target-sim/react-app.inflight.ts
index 362f8a88fd6..e930f267f72 100644
--- a/libs/wingsdk/src/target-sim/react-app.inflight.ts
+++ b/libs/wingsdk/src/target-sim/react-app.inflight.ts
@@ -13,6 +13,7 @@ export class ReactApp implements IReactAppClient, ISimulatorResourceInstance {
private readonly path: string;
private readonly environmentVariables: Record;
private readonly useBuildCommand: boolean;
+ private readonly localPort: string | number;
private childProcess?: ChildProcess;
private url: string;
@@ -22,6 +23,7 @@ export class ReactApp implements IReactAppClient, ISimulatorResourceInstance {
this.startCommand = props.startCommand;
this.environmentVariables = props.environmentVariables;
this.useBuildCommand = props.useBuildCommand;
+ this.localPort = props.localPort;
this.url = props.url;
}
@@ -56,6 +58,8 @@ window.wingEnv = ${JSON.stringify(this.environmentVariables, null, 2)};`
} else {
// react usually offer hot reloading-
// we're waiting for execution ending since it's ending only when manually terminating the process
+ options.env = { ...process.env, PORT: `${this.localPort}` };
+
this.childProcess = exec(
this.startCommand,
options,
diff --git a/libs/wingsdk/src/target-sim/react-app.ts b/libs/wingsdk/src/target-sim/react-app.ts
index 9e8dc9f52e6..825428ec240 100644
--- a/libs/wingsdk/src/target-sim/react-app.ts
+++ b/libs/wingsdk/src/target-sim/react-app.ts
@@ -17,9 +17,7 @@ export class ReactApp extends ex.ReactApp implements ISimulatorResource {
this._startCommand = this._useBuildCommand
? props.buildCommand ?? ex.DEFAULT_REACT_APP_BUILD_COMMAND
- : `PORT=${this._localPort} ${
- props.startCommand ?? DEFAULT_START_COMMAND
- }`;
+ : props.startCommand ?? DEFAULT_START_COMMAND;
if (this._useBuildCommand) {
// In the future we can create an host (proxy like) for the development one if needed
@@ -55,6 +53,7 @@ export class ReactApp extends ex.ReactApp implements ISimulatorResource {
),
useBuildCommand: this._useBuildCommand,
url: this.url,
+ localPort: this._localPort,
},
attrs: {},
};
diff --git a/libs/wingsdk/src/target-sim/schema-resources.ts b/libs/wingsdk/src/target-sim/schema-resources.ts
index 0e16c361eae..f2901973e78 100644
--- a/libs/wingsdk/src/target-sim/schema-resources.ts
+++ b/libs/wingsdk/src/target-sim/schema-resources.ts
@@ -76,6 +76,8 @@ export interface FunctionSchema extends BaseResourceSchema {
readonly environmentVariables: Record;
/** The maximum amount of time the function can run, in milliseconds. */
readonly timeout: number;
+ /** The maximum number of concurrent invocations that can run at one time. */
+ readonly concurrency: number;
};
}
@@ -263,6 +265,7 @@ export interface ReactAppSchema extends BaseResourceSchema {
environmentVariables: Record;
useBuildCommand: boolean;
url: string;
+ localPort: string | number;
};
}
export interface ReactAppAttributes {
diff --git a/libs/wingsdk/src/target-tf-aws/function.ts b/libs/wingsdk/src/target-tf-aws/function.ts
index 4ca13f8b048..fa2c17be175 100644
--- a/libs/wingsdk/src/target-tf-aws/function.ts
+++ b/libs/wingsdk/src/target-tf-aws/function.ts
@@ -11,6 +11,7 @@ import { S3Object } from "../.gen/providers/aws/s3-object";
import { SecurityGroup } from "../.gen/providers/aws/security-group";
import * as cloud from "../cloud";
import * as core from "../core";
+import { NotImplementedError } from "../core/errors";
import { createBundle } from "../shared/bundling";
import { DEFAULT_MEMORY_SIZE } from "../shared/function";
import { NameOptions, ResourceNames } from "../shared/resource-names";
@@ -86,6 +87,12 @@ export class Function extends cloud.Function implements IAwsFunction {
) {
super(scope, id, inflight, props);
+ if (props.concurrency != null) {
+ throw new NotImplementedError(
+ "Function concurrency isn't implemented yet on the current target."
+ );
+ }
+
// Create unique S3 bucket for hosting Lambda code
const app = App.of(this) as App;
const bucket = app.codeBucket;
diff --git a/libs/wingsdk/src/target-tf-azure/function.ts b/libs/wingsdk/src/target-tf-azure/function.ts
index 022d53502e0..6c321583526 100644
--- a/libs/wingsdk/src/target-tf-azure/function.ts
+++ b/libs/wingsdk/src/target-tf-azure/function.ts
@@ -79,6 +79,12 @@ export class Function extends cloud.Function {
// Create Bucket to store function code
const functionCodeBucket = new Bucket(this, "FunctionBucket");
+ if (props.concurrency != null) {
+ throw new NotImplementedError(
+ "Function concurrency isn't implemented yet on the current target."
+ );
+ }
+
// throw an error if props.memory is defined for an Azure function
if (props.memory) {
throw new NotImplementedError("memory is an invalid parameter on Azure", {
diff --git a/libs/wingsdk/src/target-tf-gcp/function.ts b/libs/wingsdk/src/target-tf-gcp/function.ts
index d14e0e2d585..43a6868af9b 100644
--- a/libs/wingsdk/src/target-tf-gcp/function.ts
+++ b/libs/wingsdk/src/target-tf-gcp/function.ts
@@ -14,6 +14,7 @@ import { ProjectIamMember } from "../.gen/providers/google/project-iam-member";
import { ServiceAccount } from "../.gen/providers/google/service-account";
import { StorageBucketObject } from "../.gen/providers/google/storage-bucket-object";
import * as cloud from "../cloud";
+import { NotImplementedError } from "../core/errors";
import { createBundle } from "../shared/bundling";
import { DEFAULT_MEMORY_SIZE } from "../shared/function";
import {
@@ -92,6 +93,12 @@ export class Function extends cloud.Function {
// app is a property of the `cloud.Function` class
const app = App.of(this) as App;
+ if (props.concurrency != null) {
+ throw new NotImplementedError(
+ "Function concurrency isn't implemented yet on the current target."
+ );
+ }
+
// memory limits must be between 128 and 8192 MB
if (props?.memory && (props.memory < 128 || props.memory > 8192)) {
throw new Error(
@@ -171,6 +178,7 @@ export class Function extends cloud.Function {
sourceArchiveObject: FunctionObjectBucket.name,
entryPoint: "handler",
triggerHttp: true,
+ httpsTriggerSecurityLevel: "SECURE_ALWAYS",
// It takes around 1 minutes to the function invocation permissions to be established -
// therefore, the timeout is higher than in other targets
timeout: props.timeout?.seconds ?? 120,
diff --git a/libs/wingsdk/src/ui/colors.ts b/libs/wingsdk/src/ui/colors.ts
new file mode 100644
index 00000000000..bbee25ec467
--- /dev/null
+++ b/libs/wingsdk/src/ui/colors.ts
@@ -0,0 +1,28 @@
+export type Colors =
+ | "orange"
+ | "sky"
+ | "emerald"
+ | "lime"
+ | "pink"
+ | "amber"
+ | "cyan"
+ | "purple"
+ | "red"
+ | "violet"
+ | "slate";
+
+export const isOfTypeColors = (keyInput?: string): keyInput is Colors => {
+ return [
+ "orange",
+ "sky",
+ "emerald",
+ "lime",
+ "pink",
+ "amber",
+ "cyan",
+ "purple",
+ "red",
+ "violet",
+ "slate",
+ ].includes(keyInput || "");
+};
diff --git a/libs/wingsdk/src/ui/index.ts b/libs/wingsdk/src/ui/index.ts
index 2c2913c3902..9024c73c68f 100644
--- a/libs/wingsdk/src/ui/index.ts
+++ b/libs/wingsdk/src/ui/index.ts
@@ -1,4 +1,5 @@
export * from "./base";
export * from "./button";
+export * from "./colors";
export * from "./field";
export * from "./section";
diff --git a/libs/wingsdk/test/core/__snapshots__/connections.test.ts.snap b/libs/wingsdk/test/core/__snapshots__/connections.test.ts.snap
index d3c2c487048..416f15e9cb7 100644
--- a/libs/wingsdk/test/core/__snapshots__/connections.test.ts.snap
+++ b/libs/wingsdk/test/core/__snapshots__/connections.test.ts.snap
@@ -47,6 +47,7 @@ return class Handler {
"attrs": {},
"path": "root/my_function",
"props": {
+ "concurrency": 100,
"environmentVariables": {},
"sourceCodeFile": ".wing/my_function_c85c4e0e.js",
"sourceCodeLanguage": "javascript",
diff --git a/libs/wingsdk/test/global.setup.ts b/libs/wingsdk/test/global.setup.ts
index 2fb3fadf648..270e39e504c 100644
--- a/libs/wingsdk/test/global.setup.ts
+++ b/libs/wingsdk/test/global.setup.ts
@@ -1,11 +1,16 @@
-import { execSync } from "child_process";
+import { execFileSync } from "child_process";
import fs from "fs/promises";
import path from "path";
export async function setup() {
- // compile src/**/*.on*.inflight.ts to .js because these are going to be
+ // compile src/**/*.ts to .js because these are going to be
// injected into our javascript vm and cannot be resolved via vitest
- execSync("pnpm tsc -p tsconfig.test.json", { stdio: "inherit" });
+ const tscPath = path.join(__dirname, "..", "node_modules", ".bin", "tsc");
+ const tsconfigPath = path.join(__dirname, "..", "tsconfig.test.json");
+ execFileSync(tscPath, ["-p", tsconfigPath], {
+ stdio: "inherit",
+ });
+
return () => {};
}
@@ -16,7 +21,7 @@ export async function teardown() {
if (process.env.WING_SDK_VITEST_SKIP_TEARDOWN) {
return;
}
- const files = await findFilesWithExtension(["src"], [".d.ts", ".js"]);
+ const files = await findFilesWithExtension(["src"], [".js"]);
for (const file of files) {
try {
await fs.unlink(file);
diff --git a/libs/wingsdk/test/simulator/__snapshots__/simulator.test.ts.snap b/libs/wingsdk/test/simulator/__snapshots__/simulator.test.ts.snap
index e875ea04279..bcb942f46ab 100644
--- a/libs/wingsdk/test/simulator/__snapshots__/simulator.test.ts.snap
+++ b/libs/wingsdk/test/simulator/__snapshots__/simulator.test.ts.snap
@@ -91,13 +91,7 @@ exports[`run single test > happy path 1`] = `
exports[`run single test > test failure 1`] = `
{
- "error": "Error: test failed
- at Handler.handle ([abs])
- at exports.handler ([abs])
- at process. ([abs])
- at process.emit (node:events:)
- at emit (node:internal/child_process:)
- at process.processTicksAndRejections (node:internal/process/task_queues:)",
+ "error": "Error: test failed",
"pass": false,
"path": "root/test",
"traces": [
diff --git a/libs/wingsdk/test/simulator/graph.test.ts b/libs/wingsdk/test/simulator/graph.test.ts
new file mode 100644
index 00000000000..8a9a033b232
--- /dev/null
+++ b/libs/wingsdk/test/simulator/graph.test.ts
@@ -0,0 +1,100 @@
+import { test, expect, describe } from "vitest";
+import { Graph } from "../../src/simulator/graph";
+
+test("empty", () => {
+ const graph = new Graph([]);
+ expect(graph.nodes.length).toBe(0);
+});
+
+test("two disconnected nodes", () => {
+ const graph = new Graph([{ path: "a" }, { path: "b" }]);
+
+ expect(graph.nodes.length).toBe(2);
+
+ const a = graph.tryFind("a")!;
+ expect(a.def).toStrictEqual({ path: "a" });
+ expect(Array.from(a.dependencies)).toStrictEqual([]);
+ expect(Array.from(a.dependents)).toStrictEqual([]);
+
+ const b = graph.tryFind("b")!;
+ expect(b.def).toStrictEqual({ path: "b" });
+ expect(Array.from(b.dependencies)).toStrictEqual([]);
+ expect(Array.from(b.dependents)).toStrictEqual([]);
+});
+
+test("explicit deps", () => {
+ const graph = new Graph([{ path: "a", deps: ["b"] }, { path: "b" }]);
+
+ const a = graph.tryFind("a")!;
+ expect(a.dependencies.size).toBe(1);
+ expect(Array.from(a.dependencies)).toStrictEqual(["b"]);
+
+ const b = graph.tryFind("b")!;
+ expect(b.dependents.size).toBe(1);
+ expect(Array.from(b.dependents)).toStrictEqual(["a"]);
+});
+
+test("implicit deps", () => {
+ const graph = new Graph([
+ {
+ path: "a",
+ props: {
+ foo: "${wsim#b#attrs.bar}",
+ another: "i depend on: ${wsim#c/d/e#attrs.xxx}",
+ },
+ },
+ { path: "b", props: { hello: ["bang", "${wsim#c/d/e#attrs.aaa}"] } },
+ { path: "c/d/e" },
+ { path: "d", props: { a: "${wsim#a#attrs.aaa}" }, deps: ["b"] },
+ ]);
+
+ const a = graph.tryFind("a")!;
+ expect(Array.from(a.dependencies)).toStrictEqual(["b", "c/d/e"]);
+ expect(Array.from(a.dependents)).toStrictEqual(["d"]);
+
+ const b = graph.tryFind("b")!;
+ expect(Array.from(b.dependencies)).toStrictEqual(["c/d/e"]);
+ expect(Array.from(b.dependents)).toStrictEqual(["a", "d"]);
+
+ const c = graph.tryFind("c/d/e")!;
+ expect(Array.from(c.dependencies)).toStrictEqual([]);
+ expect(Array.from(c.dependents)).toStrictEqual(["a", "b"]);
+
+ const d = graph.tryFind("d")!;
+ expect(Array.from(d.dependencies)).toStrictEqual(["b", "a"]);
+ expect(Array.from(d.dependents)).toStrictEqual([]);
+});
+
+test("tryFind returns undefined if node does not exist", () => {
+ const graph = new Graph([]);
+ expect(graph.tryFind("a")).toBe(undefined);
+});
+
+test("fails on a direct cyclic dependency", () => {
+ expect(() => {
+ new Graph([
+ { path: "a", deps: ["b"] },
+ { path: "b", deps: ["a"] },
+ ]);
+ }).toThrowError(/cyclic dependency detected: b -> a/);
+});
+
+test("fails on an indirect cyclic dependency", () => {
+ expect(() => {
+ new Graph([
+ { path: "a", deps: ["b"] },
+ { path: "b", deps: ["c"] },
+ { path: "c", deps: ["a"] },
+ ]);
+ }).toThrowError(/cyclic dependency detected: c -> a/);
+});
+
+test("cyclic deps introduced by token", () => {
+ expect(() => {
+ new Graph([
+ { path: "a", props: { foo: "${wsim#b#attrs.bar}" } },
+ { path: "b", props: { bar: "${wsim#c#attrs.baz}" } },
+ { path: "c", props: { baz: "${wsim#a#attrs.foo}" } },
+ ]);
+ }).toThrowError(/cyclic dependency detected: c -> a -> b -> c/);
+});
diff --git a/libs/wingsdk/test/simulator/simulator.test.ts b/libs/wingsdk/test/simulator/simulator.test.ts
index 3d80b525360..dd494bf6627 100644
--- a/libs/wingsdk/test/simulator/simulator.test.ts
+++ b/libs/wingsdk/test/simulator/simulator.test.ts
@@ -1,10 +1,23 @@
+import * as fs from "fs";
import { Construct } from "constructs";
import { test, expect, describe } from "vitest";
-import { Bucket } from "../../src/cloud";
+import {
+ Api,
+ Bucket,
+ Function,
+ IApiClient,
+ IBucketClient,
+ IFunctionClient,
+ IServiceClient,
+ OnDeploy,
+ Service,
+} from "../../src/cloud";
import { InflightBindings } from "../../src/core";
-import { Testing } from "../../src/simulator";
-import { ITestRunnerClient, Test, TestResult } from "../../src/std";
+import { Simulator, Testing } from "../../src/simulator";
+import { ITestRunnerClient, Test, TestResult, TraceType } from "../../src/std";
+import { State } from "../../src/target-sim";
import { SimApp } from "../sim-app";
+import { mkdtemp } from "../util";
describe("run single test", () => {
test("test not found", async () => {
@@ -178,6 +191,417 @@ test("provides raw tree data", async () => {
expect(treeData).toMatchSnapshot();
});
+test("unable to resolve token during initialization", async () => {
+ const app = new SimApp();
+ const state = new State(app, "State");
+ const bucket = new Bucket(app, "Bucket");
+ bucket.addObject("url.txt", state.token("my_token"));
+
+ let error;
+ try {
+ await app.startSimulator();
+ } catch (e) {
+ error = e;
+ }
+ expect(error).toBeDefined();
+ expect(error.message).toMatch(/Unable to resolve attribute 'my_token'/);
+});
+
+describe("in-place updates", () => {
+ test("no change", async () => {
+ const stateDir = mkdtemp();
+
+ const app = new SimApp();
+ new Bucket(app, "Bucket1");
+
+ const sim = await app.startSimulator(stateDir);
+ expect(sim.listResources()).toEqual(["root/Bucket1"]);
+
+ expect(simTraces(sim)).toStrictEqual(["root/Bucket1 started"]);
+
+ const app2 = new SimApp();
+ new Bucket(app2, "Bucket1");
+
+ const app2Dir = app2.synth();
+
+ await sim.update(app2Dir);
+ expect(updateTrace(sim)).toStrictEqual({
+ added: [],
+ deleted: [],
+ updated: [],
+ });
+
+ expect(simTraces(sim)).toStrictEqual([
+ "root/Bucket1 started",
+ "Update: 0 added, 0 updated, 0 deleted",
+ ]);
+
+ expect(sim.listResources()).toEqual(["root/Bucket1"]);
+ await sim.stop();
+ });
+
+ test("add", async () => {
+ const stateDir = mkdtemp();
+
+ const app = new SimApp();
+
+ new Bucket(app, "Bucket1");
+ const sim = await app.startSimulator(stateDir);
+ expect(sim.listResources()).toEqual(["root/Bucket1"]);
+ expect(simTraces(sim)).toStrictEqual(["root/Bucket1 started"]);
+
+ const app2 = new SimApp();
+ new Bucket(app2, "Bucket1");
+ new Bucket(app2, "Bucket2");
+
+ const app2Dir = app2.synth();
+ await sim.update(app2Dir);
+ expect(updateTrace(sim)).toStrictEqual({
+ added: ["root/Bucket2"],
+ deleted: [],
+ updated: [],
+ });
+
+ expect(sim.listResources()).toEqual(["root/Bucket1", "root/Bucket2"]);
+ expect(simTraces(sim)).toStrictEqual([
+ "root/Bucket1 started",
+ "Update: 1 added, 0 updated, 0 deleted",
+ "root/Bucket2 started",
+ ]);
+
+ await sim.stop();
+ });
+
+ test("delete", async () => {
+ const stateDir = mkdtemp();
+
+ const app = new SimApp();
+ new Bucket(app, "Bucket1");
+ new Bucket(app, "Bucket2");
+ const sim = await app.startSimulator(stateDir);
+ expect(sim.listResources()).toEqual(["root/Bucket1", "root/Bucket2"]);
+ expect(simTraces(sim)).toStrictEqual([
+ "root/Bucket1 started",
+ "root/Bucket2 started",
+ ]);
+
+ const app2 = new SimApp();
+ new Bucket(app2, "Bucket1");
+
+ const app2Dir = app2.synth();
+ await sim.update(app2Dir);
+ expect(updateTrace(sim)).toStrictEqual({
+ added: [],
+ deleted: ["root/Bucket2"],
+ updated: [],
+ });
+
+ expect(sim.listResources()).toEqual(["root/Bucket1"]);
+
+ expect(simTraces(sim)).toStrictEqual([
+ "root/Bucket1 started",
+ "root/Bucket2 started",
+ "Update: 0 added, 0 updated, 1 deleted",
+ "root/Bucket2 stopped",
+ ]);
+
+ await sim.stop();
+ });
+
+ test("update", async () => {
+ const stateDir = mkdtemp();
+
+ const app = new SimApp();
+ new Bucket(app, "Bucket1");
+ const sim = await app.startSimulator(stateDir);
+ expect(sim.listResources()).toEqual(["root/Bucket1"]);
+ expect(sim.getResourceConfig("root/Bucket1").props.public).toBeFalsy();
+ expect(simTraces(sim)).toStrictEqual(["root/Bucket1 started"]);
+
+ const app2 = new SimApp();
+ new Bucket(app2, "Bucket1", { public: true });
+
+ const app2Dir = app2.synth();
+ await sim.update(app2Dir);
+ expect(updateTrace(sim)).toStrictEqual({
+ added: [],
+ deleted: [],
+ updated: ["root/Bucket1"],
+ });
+
+ expect(sim.listResources()).toEqual(["root/Bucket1"]);
+
+ expect(simTraces(sim)).toStrictEqual([
+ "root/Bucket1 started",
+ "Update: 0 added, 1 updated, 0 deleted",
+ "root/Bucket1 stopped",
+ "root/Bucket1 started",
+ ]);
+
+ expect(sim.getResourceConfig("root/Bucket1").props.public).toBeTruthy();
+
+ await sim.stop();
+ });
+
+ test("add resource that depends on an existing resource", async () => {
+ const stateDir = mkdtemp();
+
+ const app = new SimApp();
+ new Bucket(app, "Bucket1");
+
+ const sim = await app.startSimulator(stateDir);
+
+ expect(simTraces(sim)).toStrictEqual(["root/Bucket1 started"]);
+
+ expect(sim.listResources()).toEqual(["root/Bucket1"]);
+ expect(sim.getResourceConfig("root/Bucket1").props.public).toBeFalsy();
+
+ const app2 = new SimApp();
+ const bucket1 = new Bucket(app2, "Bucket1");
+ const api = new Api(app2, "Api");
+ bucket1.addObject("url.txt", api.url);
+
+ const handler = `async handle() { return process.env.API_URL; }`;
+ new Function(app2, "Function", Testing.makeHandler(handler), {
+ env: { API_URL: api.url },
+ });
+
+ const app2Dir = app2.synth();
+
+ await sim.update(app2Dir);
+ expect(updateTrace(sim)).toStrictEqual({
+ added: ["root/Api", "root/Api/Endpoint", "root/Function"],
+ deleted: [],
+ updated: ["root/Bucket1"],
+ });
+
+ expect(simTraces(sim)).toStrictEqual([
+ "root/Bucket1 started",
+ "Update: 3 added, 1 updated, 0 deleted",
+ "root/Bucket1 stopped",
+ "root/Api started",
+ "root/Bucket1 started",
+ "root/Api/Endpoint started",
+ "root/Function started",
+ ]);
+
+ expect(sim.listResources()).toEqual([
+ "root/Api",
+ "root/Api/Endpoint",
+ "root/Bucket1",
+ "root/Function",
+ ]);
+
+ const bucketClient = sim.getResource("root/Bucket1") as IBucketClient;
+ const urlFromBucket = await bucketClient.get("url.txt");
+ expect(urlFromBucket.startsWith("http://127.0.0")).toBeTruthy();
+
+ const functionClient = sim.getResource("root/Function") as IFunctionClient;
+ const ret = await functionClient.invoke();
+ expect(ret).toEqual(urlFromBucket);
+ });
+
+ test("retained resource is not removed", async () => {
+ const app = new SimApp();
+ const api1 = new Api(app, "Api");
+ const bucket1 = new Bucket(app, "Bucket");
+ bucket1.addObject("url.txt", api1.url);
+
+ const stateDir = mkdtemp();
+ const sim = await app.startSimulator(stateDir);
+
+ const urlBeforeUpdate = await sim.getResource("root/Bucket").get("url.txt");
+
+ // remove the state directory otherwise Api reuses the port
+ fs.rmdirSync(sim.getResourceStateDir("/Api"), { recursive: true });
+
+ const app2 = new SimApp();
+ const api2 = new Api(app2, "Api");
+ const bucket2 = new Bucket(app2, "Bucket", { public: true }); // <-- causing the update to be updated because we are deleting the state dirtectory, so we want the file to be uploaded again.
+ bucket2.addObject("url.txt", api2.url);
+
+ const app2Dir = app2.synth();
+ await sim.update(app2Dir);
+
+ expect(updateTrace(sim)).toStrictEqual({
+ added: [],
+ deleted: [],
+ updated: ["root/Bucket"],
+ });
+
+ const urlAfterUpdate = await sim.getResource("root/Bucket").get("url.txt");
+ expect(urlBeforeUpdate).toStrictEqual(urlAfterUpdate);
+ });
+
+ test("dependent resource is replaced when a dependency is replaced", async () => {
+ const app = new SimApp();
+ const myApi = new Api(app, "Api1");
+ const myBucket = new Bucket(app, "Bucket1");
+
+ // BUCKET depends on API
+ myBucket.addObject("url.txt", myApi.url);
+
+ const stateDir = mkdtemp();
+ const sim = await app.startSimulator(stateDir);
+
+ const urlBeforeUpdate = await sim
+ .getResource("root/Bucket1")
+ .get("url.txt");
+ expect(urlBeforeUpdate.startsWith("http://127.0.0")).toBeTruthy();
+
+ expect(simTraces(sim)).toEqual([
+ "root/Api1 started",
+ "root/Api1/Endpoint started",
+ "root/Bucket1 started",
+ ]);
+
+ // now lets change some configuration of Api1. we expect the bucket to be replaced as well
+
+ const app2 = new SimApp();
+ const myApi2 = new Api(app2, "Api1", { cors: true });
+ const myBucket2 = new Bucket(app2, "Bucket1");
+ myBucket2.addObject("url.txt", myApi2.url);
+
+ // clear the state directory
+ fs.rmdirSync(stateDir, { recursive: true });
+
+ const app2Dir = app2.synth();
+ await sim.update(app2Dir);
+
+ expect(updateTrace(sim)).toStrictEqual({
+ added: [],
+ deleted: [],
+ updated: ["root/Api1"],
+ });
+
+ expect(simTraces(sim)).toEqual([
+ "root/Api1 started",
+ "root/Api1/Endpoint started",
+ "root/Bucket1 started",
+ "Update: 0 added, 1 updated, 0 deleted",
+ "root/Api1/Endpoint stopped",
+ "root/Bucket1 stopped",
+ "root/Api1 stopped",
+ "root/Api1 started",
+ "root/Api1/Endpoint started",
+ "root/Bucket1 started",
+ ]);
+
+ const urlAfterUpdate = await (
+ sim.getResource("root/Bucket1") as IBucketClient
+ ).get("url.txt");
+ expect(urlAfterUpdate).not.toEqual(urlBeforeUpdate);
+ });
+
+ test("token value is changed across an update", async () => {
+ const app = new SimApp();
+ const stateKey = "my_value";
+
+ const myState = new State(app, "State");
+
+ const myService = new Service(
+ app,
+ "Service",
+ Testing.makeHandler(
+ `async handle() { await this.myState.set("${stateKey}", "bang"); }`,
+ { myState: { obj: myState, ops: ["set"] } }
+ ),
+ { env: { VER: "1" } }
+ );
+
+ new Function(
+ app,
+ "Function",
+ Testing.makeHandler(`async handle() { return process.env.MY_VALUE; }`),
+ {
+ env: { MY_VALUE: myState.token(stateKey) },
+ }
+ );
+
+ const sim = await app.startSimulator();
+
+ const fn = sim.getResource("root/Function") as IFunctionClient;
+ const result = await fn.invoke();
+ expect(result).toEqual("bang");
+
+ // okay, now we are ready to update
+ const app2 = new SimApp();
+
+ const myState2 = new State(app2, "State");
+
+ const myService2 = new Service(
+ app2,
+ "Service",
+ Testing.makeHandler(
+ `async handle() { await this.myState.set("${stateKey}", "bing"); }`,
+ { myState: { obj: myState2, ops: ["set"] } }
+ ),
+ { env: { VER: "2" } }
+ );
+
+ new Function(
+ app2,
+ "Function",
+ Testing.makeHandler(`async handle() { return process.env.MY_VALUE; }`),
+ {
+ env: { MY_VALUE: myState.token(stateKey) },
+ }
+ );
+
+ await sim.update(app2.synth());
+
+ expect(simTraces(sim)).toEqual([
+ "root/State started",
+ "root/State.my_value = bang",
+ "root/Service started",
+ "root/Function started",
+ "Update: 0 added, 1 updated, 0 deleted",
+ "root/Service stopped",
+ "root/State.my_value = bing",
+ "root/Service started",
+ ]);
+ });
+
+ test("Construct dependencies are taken into account", async () => {
+ const app = new SimApp();
+ const handler = Testing.makeHandler(`async handle() {}`);
+ const bucket = new Bucket(app, "Bucket1");
+
+ new OnDeploy(app, "OnDeploy", handler, {
+ executeAfter: [bucket],
+ });
+
+ const sim = await app.startSimulator();
+
+ const app2 = new SimApp();
+ const bucket2 = new Bucket(app2, "Bucket1", { public: true });
+ new OnDeploy(app2, "OnDeploy", handler, {
+ executeAfter: [bucket2],
+ });
+
+ const app2Dir = app2.synth();
+ await sim.update(app2Dir);
+
+ expect(simTraces(sim)).toEqual([
+ "root/OnDeploy/Function started",
+ "root/Bucket1 started",
+ "root/OnDeploy started",
+ "Update: 0 added, 1 updated, 0 deleted",
+ "root/OnDeploy stopped",
+ "root/Bucket1 stopped",
+ "root/Bucket1 started",
+ "root/OnDeploy started",
+ ]);
+ });
+});
+
+test("tryGetResource returns undefined if the resource not found", async () => {
+ const app = new SimApp();
+ const sim = await app.startSimulator();
+ expect(sim.tryGetResource("bang")).toBeUndefined();
+ expect(sim.tryGetResourceConfig("bing")).toBeUndefined();
+});
+
function makeTest(
scope: Construct,
id: string,
@@ -191,45 +615,10 @@ function makeTest(
return new Test(scope, id, handler, bindings);
}
-function removePathsFromTraceLine(line?: string) {
- if (!line) {
- return undefined;
- }
-
- // convert wingsdk paths to src (e.g. "/a/b/wingsdk/src/z/t.js" -> "[src]/z/t.js") with relative paths
- line = line.replace(/\/.+\/wingsdk\/src\//g, "[src]/");
-
- // if any absolute paths remain, replace them with "[abs]"
- line = line.replace(/([ (])\/[^)]+/g, "$1[abs]");
-
- return line;
-}
-
-function removeLineNumbers(line?: string) {
- if (!line) {
- return undefined;
- }
-
- return line.replace(/:\d+:\d+/g, ":");
-}
-
function sanitizeResult(result: TestResult): TestResult {
let error: string | undefined;
if (result.error) {
- let lines = result.error
- .split("\n")
- .map(removePathsFromTraceLine)
- .map(removeLineNumbers);
-
- // remove all lines after "at Simulator.runTest" since they are platform-dependent
- let lastLine = lines.findIndex((line) =>
- line?.includes("Simulator.runTest")
- );
- if (lastLine !== -1) {
- lines = lines.slice(0, lastLine + 1);
- }
-
- error = lines.join("\n");
+ error = result.error.split("\n")[0];
}
return {
@@ -250,3 +639,16 @@ async function runAllTests(runner: ITestRunnerClient): Promise {
}
return results;
}
+
+function simTraces(s: Simulator) {
+ return s
+ .listTraces()
+ .filter((t) => t.type === TraceType.SIMULATOR)
+ .map((t) => t.data.message);
+}
+
+function updateTrace(s: Simulator) {
+ return s
+ .listTraces()
+ .find((t) => t.type === TraceType.SIMULATOR && t.data.update)?.data.update;
+}
diff --git a/libs/wingsdk/test/simulator/tokens.test.ts b/libs/wingsdk/test/simulator/tokens.test.ts
new file mode 100644
index 00000000000..f1c03f3ff2b
--- /dev/null
+++ b/libs/wingsdk/test/simulator/tokens.test.ts
@@ -0,0 +1,138 @@
+import { test, describe, expect } from "vitest";
+import { parseToken, resolveTokens } from "../../src/simulator/tokens";
+
+describe("parseToken", () => {
+ test("parses path", () => {
+ expect(parseToken("${wsim#foo#attrs.bar}")?.path).toBe("foo");
+ expect(parseToken("${wsim#foo/jang/bang#props.bar}")?.path).toBe(
+ "foo/jang/bang"
+ );
+ });
+
+ test("parses attribute", () => {
+ const result = parseToken("${wsim#foo/lang#attrs.bar}");
+ expect(result?.path).toBe("foo/lang");
+ expect(result?.attr).toBe("bar");
+ expect(result?.prop).toBeUndefined();
+ });
+
+ test("parses property", () => {
+ const result = parseToken("${wsim#foo#props.bar}");
+ expect(result?.path).toBe("foo");
+ expect(result?.prop).toBe("bar");
+ expect(result?.attr).toBeUndefined();
+ });
+
+ test("invalid tokens", () => {
+ expect(() => parseToken("${foo#baz}")).toThrow(/Invalid token reference/);
+ expect(() => parseToken("${wsim#foo#baz}")).toThrow(
+ /Invalid token reference/
+ );
+ });
+});
+
+describe("tryResolveTokens", () => {
+ test("undefined", () => {
+ expect(resolveTokens(undefined, () => "foo")).toBeUndefined();
+ });
+
+ test("terminal token", () => {
+ expect(
+ resolveTokens("${wsim#foo/bar#attrs.bar}", (token) => {
+ expect(token.path).toBe("foo/bar");
+ expect(token.attr).toBe("bar");
+ expect(token.prop).toBeUndefined();
+ return "resolved_token";
+ })
+ ).toBe("resolved_token");
+
+ expect(
+ resolveTokens("${wsim#foo/bar#props.bar}", (token) => {
+ expect(token.path).toBe("foo/bar");
+ expect(token.prop).toBe("bar");
+ expect(token.attr).toBeUndefined();
+ return "resolved_token_2";
+ })
+ ).toBe("resolved_token_2");
+ });
+
+ test("nested token inside a string", () => {
+ expect(
+ resolveTokens(
+ "hello, I am a ${wsim#foo/bar#attrs.tttt} inside a ${wsim#bing/bang#props.vvv}",
+ (token) => {
+ if (token.path === "foo/bar" && token.attr === "tttt") {
+ return "cool nested token";
+ }
+
+ if (token.path === "bing/bang" && token.prop === "vvv") {
+ return "cool string";
+ }
+
+ expect.fail(`unexpected token: ${JSON.stringify(token)}`);
+ }
+ )
+ ).toBe("hello, I am a cool nested token inside a cool string");
+ });
+
+ test("tokens within an array", () => {
+ const result = resolveTokens(
+ [
+ "bla",
+ "${wsim#foo/bar#attrs.tttt}",
+ "blabla",
+ "nested nested ${wsim#bing/bang#props.vvv} nested",
+ ],
+ (token) => {
+ if (token.path === "foo/bar" && token.attr === "tttt") {
+ return "T1";
+ }
+
+ if (token.path === "bing/bang" && token.prop === "vvv") {
+ return "T2";
+ }
+
+ expect.fail(`unexpected token: ${JSON.stringify(token)}`);
+ }
+ );
+
+ expect(result).toEqual(["bla", "T1", "blabla", "nested nested T2 nested"]);
+ });
+
+ test("tokens within an object", () => {
+ const result = resolveTokens(
+ {
+ key1: "bla",
+ key2: "${wsim#foo/bar#attrs.tttt}",
+ key3: {
+ bang: ["nested nested ${wsim#bing/bang#props.vvv} nested"],
+ bing: {
+ jone: "${wsim#foo/bar#attrs.tttt}",
+ },
+ },
+ },
+ (token) => {
+ if (token.path === "foo/bar" && token.attr === "tttt") {
+ return "T1";
+ }
+
+ if (token.path === "bing/bang" && token.prop === "vvv") {
+ return "T2";
+ }
+
+ expect.fail(`unexpected token: ${JSON.stringify(token)}`);
+ }
+ );
+
+ expect(result).toEqual({
+ key1: "bla",
+ key2: "T1",
+ key3: {
+ bang: ["nested nested T2 nested"],
+ bing: {
+ jone: "T1",
+ },
+ },
+ });
+ });
+});
diff --git a/libs/wingsdk/test/target-sim/__snapshots__/api.test.ts.snap b/libs/wingsdk/test/target-sim/__snapshots__/api.test.ts.snap
index 024e61fe721..99448498f90 100644
--- a/libs/wingsdk/test/target-sim/__snapshots__/api.test.ts.snap
+++ b/libs/wingsdk/test/target-sim/__snapshots__/api.test.ts.snap
@@ -52,6 +52,7 @@ return class Handler {
"attrs": {},
"path": "root/my_api/OnRequestHandler0",
"props": {
+ "concurrency": 100,
"environmentVariables": {},
"sourceCodeFile": ".wing/onrequesthandler0_c82d41b2.js",
"sourceCodeLanguage": "javascript",
@@ -97,6 +98,10 @@ return class Handler {
{
"addr": "c8a199e7bc539eb442a47653fe0f2b1f0bf50aa6d1",
"attrs": {},
+ "deps": [
+ "root/my_api/OnRequestHandler0",
+ "root/my_api",
+ ],
"path": "root/my_api/ApiEventMapping0",
"props": {
"publisher": "\${wsim#root/my_api#attrs.handle}",
@@ -224,7 +229,6 @@ return class Handler {
},
"display": {
"description": "Api root/my_api",
- "hidden": true,
"title": "Endpoint",
},
"id": "Endpoint",
@@ -271,20 +275,19 @@ return class Handler {
exports[`api handler can read the request params 1`] = `
[
- "root/my_api/Endpoint is waiting on a dependency",
- "@winglang/sdk.cloud.Function created.",
"Server listening on http://127.0.0.1:",
- "@winglang/sdk.cloud.Api created.",
- "@winglang/sdk.sim.EventMapping created.",
- "@winglang/sdk.cloud.Endpoint created.",
+ "root/my_api started",
+ "root/my_api/Endpoint started",
+ "root/my_api/OnRequestHandler0 started",
+ "root/my_api/ApiEventMapping0 started",
"Processing "GET /hello" params={}).",
"Invoke (payload="{\\"headers\\":{\\"host\\":\\"127.0.0.1:\\",\\"connection\\":\\"keep-alive\\",\\"accept\\":\\"*/*\\",\\"accept-language\\":\\"*\\",\\"sec-fetch-mode\\":\\"cors\\",\\"user-agent\\":\\"node\\",\\"accept-encoding\\":\\"gzip, deflate\\"},\\"body\\":\\"\\",\\"method\\":\\"GET\\",\\"path\\":\\"/hello\\",\\"query\\":{\\"foo\\":\\"bar\\",\\"bar\\":\\"baz\\"},\\"vars\\":{}}").",
"GET /hello - 200.",
- "@winglang/sdk.sim.EventMapping deleted.",
+ "root/my_api/Endpoint stopped",
+ "root/my_api/ApiEventMapping0 stopped",
+ "root/my_api/OnRequestHandler0 stopped",
"Closing server on http://127.0.0.1:",
- "@winglang/sdk.cloud.Api deleted.",
- "@winglang/sdk.cloud.Function deleted.",
- "@winglang/sdk.cloud.Endpoint deleted.",
+ "root/my_api stopped",
]
`;
@@ -335,6 +338,7 @@ return class Handler {
"attrs": {},
"path": "root/my_api/OnRequestHandler0",
"props": {
+ "concurrency": 100,
"environmentVariables": {},
"sourceCodeFile": ".wing/onrequesthandler0_c82d41b2.js",
"sourceCodeLanguage": "javascript",
@@ -370,6 +374,10 @@ return class Handler {
{
"addr": "c8a199e7bc539eb442a47653fe0f2b1f0bf50aa6d1",
"attrs": {},
+ "deps": [
+ "root/my_api/OnRequestHandler0",
+ "root/my_api",
+ ],
"path": "root/my_api/ApiEventMapping0",
"props": {
"publisher": "\${wsim#root/my_api#attrs.handle}",
@@ -493,7 +501,6 @@ return class Handler {
},
"display": {
"description": "Api root/my_api",
- "hidden": true,
"title": "Endpoint",
},
"id": "Endpoint",
@@ -540,20 +547,19 @@ return class Handler {
exports[`api handler can read the request path 1`] = `
[
- "root/my_api/Endpoint is waiting on a dependency",
- "@winglang/sdk.cloud.Function created.",
"Server listening on http://127.0.0.1:",
- "@winglang/sdk.cloud.Api created.",
- "@winglang/sdk.sim.EventMapping created.",
- "@winglang/sdk.cloud.Endpoint created.",
+ "root/my_api started",
+ "root/my_api/Endpoint started",
+ "root/my_api/OnRequestHandler0 started",
+ "root/my_api/ApiEventMapping0 started",
"Processing "GET /hello" params={}).",
"Invoke (payload="{\\"headers\\":{\\"host\\":\\"127.0.0.1: