Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update spin 3 docs to represent js sdk v3 structure #1472

Merged
merged 2 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 13 additions & 34 deletions content/spin/v3/ai-sentiment-analysis-api-tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,16 +451,8 @@ impl FromStr for Sentiment {
{{ startTab "TypeScript"}}

```typescript
import {
HandleRequest,
HttpRequest,
HttpResponse,
Llm,
InferencingModels,
InferencingOptions,
Router,
Kv,
} from "@fermyon/spin-sdk";
import { Llm, Kv } from "@fermyon/spin-sdk";
import { AutoRouter } from 'itty-router';

interface SentimentAnalysisRequest {
sentence: string;
Expand Down Expand Up @@ -492,9 +484,9 @@ negative
<SENTENCE>
`;

async function performSentimentAnalysis(request: HttpRequest) {
async function performSentimentAnalysis(request: Request) {
// Parse sentence out of request
let data = request.json() as SentimentAnalysisRequest;
let data = await request.json() as SentimentAnalysisRequest;
let sentence = data.sentence;
console.log("Performing sentiment analysis on: " + sentence);

Expand All @@ -515,9 +507,9 @@ async function performSentimentAnalysis(request: HttpRequest) {

// Otherwise, perform sentiment analysis
console.log("Running inference");
let options: InferencingOptions = { maxTokens: 6 };
let options: Llm.InferencingOptions = { maxTokens: 6 };
let inferenceResult = Llm.infer(
InferencingModels.Llama2Chat,
Llm.InferencingModels.Llama2Chat,
PROMPT.replace("<SENTENCE>", sentence),
options
);
Expand All @@ -539,36 +531,23 @@ async function performSentimentAnalysis(request: HttpRequest) {
console.log("Caching sentiment in KV store");
kv.set(sentence, sentiment);

return {
status: 200,
body: JSON.stringify({
return new Response(JSON.stringify({
sentiment,
} as SentimentAnalysisResponse),
};
} as SentimentAnalysisResponse), { headers: { "Content-Type": "application/json" }});
}

let router = Router();
let router = AutoRouter();

// Map the route to the handler
router.post("/api/sentiment-analysis", async (_, req) => {
router.post("/api/sentiment-analysis", async (req) => {
console.log(`${new Date().toISOString()} POST /sentiment-analysis`);
return await performSentimentAnalysis(req);
});

// Catch all 404 handler
router.all("/api/*", async (_, req) => {
return {
status: 404,
body: "Not found",
};
//@ts-ignore
addEventListener('fetch', async (event: FetchEvent) => {
event.respondWith(router.fetch(event.request));
});

// Entry point to the Spin handler
export const handleRequest: HandleRequest = async function (
request: HttpRequest
): Promise<HttpResponse> {
return await router.handleRequest(request, request);
};
```

{{ blockEnd }}
Expand Down
4 changes: 2 additions & 2 deletions content/spin/v3/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,12 @@ It's normally convenient to put the detailed build instructions in `package.json
```json
{
"scripts": {
"build": "npx webpack --mode=production && npx mkdirp target && npx j2w -i dist.js -d combined-wit -n combined -o target/spin-http-js.wasm"
"build": "knitwit --out-dir build/wit/knitwit --out-world combined && npx webpack --mode=production && npx mkdirp target && npx j2w -i dist.js -d combined-wit -n combined -o target/spin-http-js.wasm"
}
}
```

{{ details "Parts of the build script" "The build script calls out to [`webpack`](https://webpack.js.org/) and `j2w` which is a script provided by the `@fermyon/spin-sdk` package that utilizes [`ComponentizeJS`](https://github.com/bytecodealliance/ComponentizeJS)."}}
{{ details "Parts of the build script" "The build script calls out to [`webpack`](https://webpack.js.org/) and `j2w` which is a script provided by the `@fermyon/spin-sdk` package that utilizes [`ComponentizeJS`](https://github.com/bytecodealliance/ComponentizeJS). [`knitwit`](https://github.com/fermyon/knitwit) is a utility that helps combine `wit` worlds "}}

The build command can then call the NPM script:

Expand Down
138 changes: 60 additions & 78 deletions content/spin/v3/javascript-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/javascript
- [HTTP Components](#http-components)
- [Sending Outbound HTTP Requests](#sending-outbound-http-requests)
- [Storing Data in Redis From JS/TS Components](#storing-data-in-redis-from-jsts-components)
- [Routing in a Component](#routing-in-a-component)
- [Storing Data in the Spin Key-Value Store](#storing-data-in-the-spin-key-value-store)
- [Storing Data in SQLite](#storing-data-in-sqlite)
- [Storing Data in MySQL and PostgreSQL Relational Databases](#storing-data-in-mysql-and-postgresql-relational-databases)
Expand Down Expand Up @@ -76,13 +75,13 @@ This creates a directory of the following structure:
<!-- @nocpy -->

```text
hello-world/
├── knitwit.json
hello-world
├── config
│ └── knitwit.json
├── package.json
├── spin.toml
├── src
│   ├── index.ts
│   └── spin.ts
│ └── index.ts
├── tsconfig.json
└── webpack.config.js
```
Expand Down Expand Up @@ -134,47 +133,53 @@ for writing Spin components with the Spin JS/TS SDK.
> Make sure to read [the page describing the HTTP trigger](./http-trigger.md) for more
> details about building HTTP applications.

Building a Spin HTTP component using the JS/TS SDK means writing a single function
that takes an HTTP request and a Response Builder which can be used to return an HTTP response as a parameter.
Building a Spin HTTP component with the JavaScript/TypeScript SDK now involves adding an event listener for the `fetch` event. This event listener handles incoming HTTP requests and allows you to construct and return HTTP responses.

Below is a complete implementation for such a component in TypeScript:

```javascript
import { ResponseBuilder } from "@fermyon/spin-sdk";
import { AutoRouter } from 'itty-router';

export async function handler(req: Request, res: ResponseBuilder) {
console.log(req);
res.send("hello universe");
}
```
let router = AutoRouter();

router
.get("/", () => new Response("hello universe"))
.get('/hello/:name', ({ name }) => `Hello, ${name}!`)

The important things to note in the implementation above:
//@ts-ignore
addEventListener('fetch', async (event: FetchEvent) => {
event.respondWith(router.fetch(event.request));
});

- The `handler` function is the entry point for the Spin component.
- The execution of the function terminates once `res.send` or `res.end` is called.
```

## Sending Outbound HTTP Requests

If allowed, Spin components can send outbound HTTP requests.
Let's see an example of a component that makes a request to [an API that returns random animal facts](https://random-data-api.fermyon.app/animals/json)

```javascript
import { ResponseBuilder } from "@fermyon/spin-sdk";

interface AnimalFact {
timestamp: number;
fact: string;
}
import { AutoRouter } from 'itty-router';

export async function handler(req: Request, res: ResponseBuilder) {
const animalFactResponse = await fetch("https://random-data-api.fermyon.app/animals/json")
const animalFact = await animalFactResponse.json() as AnimalFact
let router = AutoRouter();

const body = `Here's an animal fact: ${animalFact.fact}\n`
router
.get("*", getDataFromAPI)

res.set({"content-type": "text/plain"})
res.send(body)
async function getDataFromAPI(_request: Request) {
let response = await fetch(
'https://random-data-api.fermyon.app/physics/json',
);
let data = await response.json();
let fact = `Here is a fact: ${data.fact}`;
return new Response(fact);
}

//@ts-ignore
addEventListener('fetch', async (event: FetchEvent) => {
event.respondWith(router.fetch(event.request));
});

```

Before we can execute this component, we need to add the `random-data-api.fermyon.app`
Expand Down Expand Up @@ -216,7 +221,7 @@ content-type: application/json; charset=utf-8
content-length: 185
server: spin/0.1.0

Here's an animal fact: Reindeer grow new antlers every year
Here is a fact: Reindeer grow new antlers every year
```

> Without the `allowed_outbound_hosts` field populated properly in `spin.toml`,
Expand Down Expand Up @@ -245,31 +250,35 @@ Using the Spin's JS SDK, you can use the Redis key/value store and to publish me
Let's see how we can use the JS/TS SDK to connect to Redis:

```javascript
import { ResponseBuilder, Redis } from '@fermyon/spin-sdk';
import { AutoRouter } from 'itty-router';
import { Redis } from '@fermyon/spin-sdk';

const encoder = new TextEncoder();
const decoder = new TextDecoder();
const redisAddress = 'redis://localhost:6379/';

export async function handler(_req: Request, res: ResponseBuilder) {
try {
let db = Redis.open(redisAddress);
db.set('test', encoder.encode('Hello world'));
let val = db.get('test');

if (!val) {
res.status(404);
res.send();
return;
}
// publish to a channel names "message"
db.publish("message", val)
res.send(val);
} catch (e: any) {
res.status(500);
res.send(`Error: ${JSON.stringify(e.payload)}`);
}
}
let router = AutoRouter();

router
.get("/", () => {
try {
let db = Redis.open(redisAddress);
db.set('test', encoder.encode('Hello world'));
let val = db.get('test');

if (!val) {
return new Response(null, { status: 404 });
}
return new Response(val);
} catch (e: any) {
return new Response(`Error: ${JSON.stringify(e.payload)}`, { status: 500 });
}
})

//@ts-ignore
addEventListener('fetch', async (event: FetchEvent) => {
event.respondWith(router.fetch(event.request));
});

```

This HTTP component demonstrates fetching a value from Redis by key, setting a key with a value, and publishing a message to a Redis channel.
Expand All @@ -285,33 +294,6 @@ As with all networking APIs, you must grant access to Redis hosts via the `allow
allowed_outbound_hosts = ["redis://localhost:6379"]
```

## Routing in a Component

The JavaScript/TypeScript SDK provides a router that makes it easier to handle routing within a component. The router is based on [`itty-router`](https://www.npmjs.com/package/itty-router). An additional function `handleRequest` has been implemented in the router to allow passing in the Spin HTTP request directly. An example usage of the router is given below:

```javascript
import { ResponseBuilder, Router } from '@fermyon/spin-sdk';

let router = Router();

router.get("/", (_, req, res) => { handleDefaultRoute(req, res) })
router.get("/home/:id", (metadata, req, res) => { handleHomeRoute(req, res, metadata.params.id) })

async function handleDefaultRoute(_req: Request, res: ResponseBuilder) {
res.set({ "content-type": "text/plain" });
res.send("Hello from default route");
}

async function handleHomeRoute(_req: Request, res: ResponseBuilder, id: string) {
res.set({ "content-type": "text/plain" });
res.send(`Hello from home route with id: ${id}`);
}

export async function handler(req: Request, res: ResponseBuilder) {
await router.handleRequest(req, res);
}
```

## Storing Data in the Spin Key-Value Store

Spin has a key-value store built in. For information about using it from TypeScript/JavaScript, see [the key-value store tutorial](key-value-store-tutorial).
Expand Down Expand Up @@ -381,5 +363,5 @@ These are some of the suggested libraries that have been tested and confirmed to

## Caveats

- All `spin-sdk` related functions and methods (like `Variables`, `Redis`, `Mysql`, `Pg`, `Kv` and `Sqlite`) can be called only inside the `handler` function. This includes `fetch`. Any attempts to use it outside the function will lead to an error. This is due to Wizer using only Wasmtime to execute the script at build time, which does not include any Spin SDK support.
- All `spin-sdk` related functions and methods (like `Variables`, `Redis`, `Mysql`, `Pg`, `Kv` and `Sqlite`) can be called only inside the fetch event handler. This includes `fetch`. Any attempts to use it outside the function will lead to an error. This is due to Wizer using only Wasmtime to execute the script at build time, which does not include any Spin SDK support.
- No crypto operation that involve handling private keys are supported.
Loading