From ec3ad1cb2fc23a2e600a1f72d82930a008aa47b1 Mon Sep 17 00:00:00 2001 From: Toshit Chawda <45221816+r58Playz@users.noreply.github.com> Date: Sun, 25 Oct 2026 17:52:45 +0530 Subject: [PATCH] docs: easy/medium/hard networking recipes (#4040) * add net category + http request recipes * add tcp/tls socket recipes * add complex recipes * minor refine --------- Co-authored-by: Reynaldi Chernando --- src/docs/src/recipe-tags.js | 1 + src/docs/src/recipes/build-an-rss-reader.md | 153 +++++++++++++++++++ src/docs/src/recipes/check-website-uptime.md | 153 +++++++++++++++++++ src/docs/src/recipes/connect-to-a-server.md | 152 ++++++++++++++++++ src/docs/src/recipes/make-http-requests.md | 112 ++++++++++++++ 5 files changed, 571 insertions(+) create mode 100644 src/docs/src/recipes/build-an-rss-reader.md create mode 100644 src/docs/src/recipes/check-website-uptime.md create mode 100644 src/docs/src/recipes/connect-to-a-server.md create mode 100644 src/docs/src/recipes/make-http-requests.md diff --git a/src/docs/src/recipe-tags.js b/src/docs/src/recipe-tags.js index dacb25057..8efa21ea0 100644 --- a/src/docs/src/recipe-tags.js +++ b/src/docs/src/recipe-tags.js @@ -18,6 +18,7 @@ const recipeTags = { teams: 'Teams', performance: 'Performance', 'data-modeling': 'Data Modeling', + net: 'Networking', }; module.exports = recipeTags; diff --git a/src/docs/src/recipes/build-an-rss-reader.md b/src/docs/src/recipes/build-an-rss-reader.md new file mode 100644 index 000000000..7f25c4344 --- /dev/null +++ b/src/docs/src/recipes/build-an-rss-reader.md @@ -0,0 +1,153 @@ +--- +title: Build an RSS Reader +description: Learn how to build an RSS reader with Puter.js, so users can read the latest headlines from any feed. +tags: [net, kv] +order: 70 +--- + +An RSS reader fetches a feed and shows links to its latest articles. Use +[`puter.net.fetch()`](/Networking/fetch/) to read feeds that do not allow +cross-origin requests, then cache the headlines with +[`puter.kv`](/KV/) so reopening the app does not fetch the same feed again. + +This example keeps up to 20 headlines for five minutes and lets the user +refresh them sooner. The cache belongs to the signed-in user, inside your +app's key-value store. The first request opens the Puter sign-in window if +the user isn't signed in yet. + +## Read the Feed + +Fetch the XML, check the HTTP status, and parse it with the browser's +[DOMParser](https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString). +Read the title and link from each RSS `item`. Because DOMParser is a browser +API, this code runs in your app's page: + +```js +async function readFeed(feedUrl) { + const response = await puter.net.fetch(feedUrl); + if (!response.ok) { + throw new Error(`Could not load the feed (HTTP ${response.status}).`); + } + + const xml = await response.text(); + const feed = new DOMParser().parseFromString(xml, 'application/xml'); + if (feed.querySelector('parsererror') || feed.documentElement.localName !== 'rss') { + throw new Error('Expected a valid RSS feed.'); + } + + const articles = []; + for (const item of feed.querySelectorAll('channel > item')) { + const link = item.querySelector('link')?.textContent.trim(); + if (!link) continue; + + let url; + try { + url = new URL(link, response.url || feedUrl); + } catch { + continue; + } + if (url.protocol !== 'https:' && url.protocol !== 'http:') continue; + + articles.push({ + title: (item.querySelector('title')?.textContent.trim() || 'Untitled').slice(0, 200), + url: url.href, + }); + if (articles.length === 20) break; + } + + return articles; +} +``` + +The function returns plain objects that can go straight into KV. Items +without a valid HTTP or HTTPS link are left out. Relative links are resolved +against the feed's URL. + +This parser reads RSS feeds with `channel` and `item` elements. Atom feeds use +a different format and need a different parser. + +## Cache the Headlines + +Use [`puter.kv.get()`](/KV/get/) to look for a cached result before fetching. +Pass an expiry to [`puter.kv.set()`](/KV/set/) so it goes away after five +minutes. The expiry is a Unix timestamp in seconds: + +```js +async function loadFeed(feedUrl, refresh = false) { + const key = `rss:cache:${feedUrl}`; + if (!refresh) { + const cached = await puter.kv.get(key); + if (cached !== null) return cached; + } + + const articles = await readFeed(feedUrl); + const inFiveMinutes = Math.floor(Date.now() / 1000) + 5 * 60; + await puter.kv.set(key, articles, inFiveMinutes); + return articles; +} +``` + +A missing or expired key returns `null`. An empty feed returns an empty +array, which is cached too. Passing `true` for `refresh` fetches a new copy +and replaces the cache after the feed has been read successfully. + +Only titles and links are cached, which keeps each value well under the +[KV value size limit](/KV/MAX_VALUE_SIZE/). + +## Show the Headlines + +Add these elements to a page that loads Puter.js: + +```html + +

+ +``` + +Put the two functions above and this code in the same script, after the +elements. It loads a BBC News feed on opening and fetches a fresh copy when +the user clicks Refresh: + +```js +const feedUrl = 'https://feeds.bbci.co.uk/news/rss.xml'; +const refreshButton = document.getElementById('refresh-feed'); +const status = document.getElementById('feed-status'); +const list = document.getElementById('headlines'); + +async function showFeed(refresh = false) { + refreshButton.disabled = true; + status.textContent = 'Loading...'; + + try { + const articles = await loadFeed(feedUrl, refresh); + const rows = articles.map((article) => { + const row = document.createElement('li'); + const link = document.createElement('a'); + link.textContent = article.title; + link.href = article.url; + row.append(link); + return row; + }); + + list.replaceChildren(...rows); + status.textContent = articles.length ? 'Headlines loaded.' : 'No articles in this feed.'; + } catch (error) { + const message = error?.message ?? error?.error?.message ?? error; + status.textContent = `Could not load the headlines: ${message}`; + } finally { + refreshButton.disabled = false; + } +} + +refreshButton.addEventListener('click', () => showFeed(true)); +showFeed(); +``` + +Titles are assigned through `textContent`, so markup in a feed stays text. +If a refresh fails, the current headlines stay on the page and the status +shows the error. + +## Notes + +- To keep full articles or other large feed data, save it as files instead. + See [store files](/recipes/store-files/). diff --git a/src/docs/src/recipes/check-website-uptime.md b/src/docs/src/recipes/check-website-uptime.md new file mode 100644 index 000000000..59c4e4708 --- /dev/null +++ b/src/docs/src/recipes/check-website-uptime.md @@ -0,0 +1,153 @@ +--- +title: Check Website Uptime +description: Learn how to check whether a website is up with Puter.js and keep a history of recent checks for each user. +tags: [net, kv] +order: 71 +--- + +To see whether a website is responding, make a request with +[`puter.net.fetch()`](/Networking/fetch/) and save the result with +[`puter.kv.set()`](/KV/set/). This gives your app a history of successful +checks, HTTP errors and connection failures. + +This example checks a site when the user clicks a button, keeps each result +for seven days, and shows the latest 20 checks. Each user's history lives in +their own account. The first check opens the Puter sign-in window if the user +isn't signed in yet. + +## Check the Site + +Use a `HEAD` request to read the site's status without downloading its page. +Check `response.ok` to tell whether it returned a status from `200` to `299`, +and catch request failures so they can be saved too: + +```js +async function checkWebsite(url) { + const started = performance.now(); + const result = { + at: Date.now(), + up: false, + status: null, + durationMs: 0, + error: null, + }; + + try { + const response = await puter.net.fetch(url, { method: 'HEAD' }); + result.up = response.ok; + result.status = response.status; + } catch (error) { + result.error = String(error?.message ?? error?.error?.message ?? error); + } + result.durationMs = Math.round(performance.now() - started); + + const prefix = `uptime:${encodeURIComponent(url)}:`; + const key = `${prefix}${new Date(result.at).toISOString()}:${crypto.randomUUID()}`; + const inSevenDays = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60; + await puter.kv.set(key, result, inSevenDays); + return result; +} +``` + +An HTTP error has a status, such as `503`. A request that fails before getting +a response has `status: null` and an error message. A failed KV write still +rejects, so the app can tell the user when a result couldn't be saved. + +This example treats every status outside `200` to `299` as down. Change that +rule if your endpoint uses another status for a successful check. Use an +endpoint that accepts `HEAD`, because a site that rejects it can return `405` +even when its pages work. For a `GET` check, cancel the response body with +`await response.body?.cancel()` after reading the status. + +The elapsed time includes connection setup and the network path from the +user's browser, so each result is one sample of the site's availability. + +Each check has its own key. The timestamp sorts checks by time, and the UUID +keeps two checks made in the same millisecond from replacing each other. +The third argument to [`puter.kv.set()`](/KV/set/) is an expiry timestamp in +seconds. + +## Read Recent Checks + +List the site's keys with [`puter.kv.list()`](/KV/list/). Pass `reverse: true` +to put the newest first and `limit: 20` to read one page of history: + +```js +async function getHistory(url) { + const page = await puter.kv.list({ + pattern: `uptime:${encodeURIComponent(url)}:*`, + returnValues: true, + reverse: true, + limit: 20, + fetchUntilFull: true, + }); + + return page.items.map((entry) => entry.value); +} +``` + +With `limit`, [`puter.kv.list()`](/KV/list/) returns a page object whose +`items` hold the key-value pairs. The `fetchUntilFull` option fills the page +when possible, even if expired keys are skipped. A site with no saved checks +returns an empty list. + +To show older results, keep the page's `cursor` and request the next page, as +in [store a large collection](/recipes/store-large-collection/). + +## Show the History + +Add a button, a status message and a place for the history to a page that +loads Puter.js: + +```html + +

+

+```
+
+Put the two functions above and this code in the same script, after the
+elements. Change `targetUrl` to the site or health endpoint you want to check:
+
+```js
+const targetUrl = 'https://example.com/';
+const checkButton = document.getElementById('check-site');
+const status = document.getElementById('check-status');
+const history = document.getElementById('check-history');
+
+async function showHistory() {
+    const checks = await getHistory(targetUrl);
+    history.textContent = checks.length ? checks.map((check) => {
+        const time = new Date(check.at).toLocaleString();
+        const outcome = check.up ? 'Up' : 'Down';
+        const detail = check.status === null ? check.error : `HTTP ${check.status}`;
+        return `${time} - ${outcome} - ${detail} - ${check.durationMs} ms`;
+    }).join('\n') : 'No checks yet.';
+}
+
+checkButton.addEventListener('click', async () => {
+    checkButton.disabled = true;
+    status.textContent = 'Checking...';
+
+    try {
+        const result = await checkWebsite(targetUrl);
+        status.textContent = result.up ? 'The site responded successfully.' : 'The check failed.';
+        await showHistory();
+    } catch (error) {
+        const message = error?.message ?? error?.error?.message ?? error;
+        status.textContent = `Could not save or load the results: ${message}`;
+    } finally {
+        checkButton.disabled = false;
+    }
+});
+
+showHistory().catch((error) => {
+    const message = error?.message ?? error?.error?.message ?? error;
+    status.textContent = `Could not load the history: ${message}`;
+});
+```
+
+The button stays disabled until the check and history refresh finish. A
+connection failure is saved as a failed check, and its message appears in
+the history. Checks run only when the user clicks the button, so closing the
+page stops them.
+
diff --git a/src/docs/src/recipes/connect-to-a-server.md b/src/docs/src/recipes/connect-to-a-server.md
new file mode 100644
index 000000000..a373f946e
--- /dev/null
+++ b/src/docs/src/recipes/connect-to-a-server.md
@@ -0,0 +1,152 @@
+---
+title: Connect to a Server
+description: Learn how to open TCP and TLS connections from the browser with Puter.js, so your app can talk to any server on any port.
+tags: [net]
+order: 66
+---
+
+To talk to a server over a TCP connection, use the
+[`puter.net.Socket()`](/Networking/Socket/) constructor. For an encrypted
+connection, use [`puter.net.tls.TLSSocket()`](/Networking/TLSSocket/).
+Both take a hostname and a port, then send and receive data through events.
+Browsers can't open raw TCP connections on their own, so these sockets give
+your app's page direct access to servers such as mail, database and WHOIS
+servers. The first connection opens the Puter sign-in window if the user
+isn't signed in yet.
+
+For most HTTP requests, [use `puter.net.fetch()`](/recipes/make-http-requests/).
+A socket is useful when you need to send the protocol's bytes yourself.
+
+## Send an HTTP Request
+
+To connect to an HTTP server, pass its hostname and port `80`. Wait for the
+`open` event, then send the request with the
+[`write()`](/Networking/Socket/) method:
+
+```js
+const socket = new puter.net.Socket('example.com', 80);
+const decoder = new TextDecoder();
+
+socket.on('open', () => {
+    socket.write('GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n');
+});
+
+socket.on('data', (data) => {
+    console.log(decoder.decode(data, { stream: true }));
+});
+
+socket.on('error', (error) => {
+    console.error('Connection failed:', error.message);
+});
+
+socket.on('close', () => {
+    const remaining = decoder.decode();
+    if (remaining) console.log(remaining);
+});
+```
+
+The server sends bytes in `data` events. This prints the HTTP response,
+including its headers. The `Connection: close` header asks the server to close
+the connection after sending it. Each `\r\n` ends a line, and the empty line
+ends the request headers.
+
+Passing `{ stream: true }` to `TextDecoder` keeps a character that's split
+across two chunks intact. The last `decode()` call in the `close` handler
+flushes any remaining text.
+
+## Use TLS
+
+To make the same request over an encrypted connection, use
+[`puter.net.tls.TLSSocket()`](/Networking/TLSSocket/) and port `443`.
+Its connection events are `tlsopen`, `tlsdata` and `tlsclose`. Errors still
+use `error`:
+
+```js
+const socket = new puter.net.tls.TLSSocket('example.com', 443);
+const decoder = new TextDecoder();
+
+socket.on('tlsopen', () => {
+    socket.write('GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n');
+});
+
+socket.on('tlsdata', (data) => {
+    console.log(decoder.decode(data, { stream: true }));
+});
+
+socket.on('error', (error) => {
+    console.error('Connection failed:', error.message);
+});
+
+socket.on('tlsclose', () => {
+    const remaining = decoder.decode();
+    if (remaining) console.log(remaining);
+});
+```
+
+Use a TLS socket on a port where the server accepts TLS connections.
+
+## Send Bytes
+
+The [`write()`](/Networking/Socket/) method also accepts a `Uint8Array` or an
+`ArrayBuffer`.
+Use `TextEncoder` to turn text into UTF-8 bytes. This example asks the
+[IANA WHOIS server](https://www.iana.org/whois) for information about `.com`:
+
+```js
+const socket = new puter.net.Socket('whois.iana.org', 43);
+const encoder = new TextEncoder();
+const decoder = new TextDecoder();
+let reply = '';
+
+socket.on('open', () => {
+    socket.write(encoder.encode('com\r\n'));
+});
+
+socket.on('data', (data) => {
+    reply += decoder.decode(data, { stream: true });
+});
+
+socket.on('error', (error) => {
+    console.error('WHOIS request failed:', error.message);
+});
+
+socket.on('close', (hadError) => {
+    reply += decoder.decode();
+    if (!hadError) console.log(reply);
+});
+```
+
+[WHOIS](https://www.rfc-editor.org/rfc/rfc3912) uses port `43`, takes a query
+ending in `\r\n`, and closes the connection when the reply is complete.
+The example collects all the chunks before printing it.
+
+A `data` event can contain part of a message or several messages, so use the
+protocol's rules to decide when a reply is complete. For a binary protocol,
+work with the received `Uint8Array` directly and decode only the bytes that
+the protocol defines as text.
+
+## Handle Errors and Close
+
+A socket connects after you create it. Listen for `error` to handle a failed
+connection, and for `close` to know when it ends. The `hadError` argument is
+`true` when the connection closes because of an error:
+
+```js
+const socket = new puter.net.Socket('does-not-exist.invalid', 80);
+
+socket.on('error', (error) => {
+    console.error('Connection failed:', error.message);
+});
+
+socket.on('close', (hadError) => {
+    console.log('Connection closed. Had an error:', hadError);
+});
+```
+
+To close a connection yourself, call the [`close()`](/Networking/Socket/)
+method on the socket you opened:
+
+```js
+socket.close();
+```
+
diff --git a/src/docs/src/recipes/make-http-requests.md b/src/docs/src/recipes/make-http-requests.md
new file mode 100644
index 000000000..cc8052c1d
--- /dev/null
+++ b/src/docs/src/recipes/make-http-requests.md
@@ -0,0 +1,112 @@
+---
+title: Make HTTP Requests
+description: Learn how to make HTTP requests from the browser with Puter.js, so your app can call any API or website, even ones that block cross-origin requests.
+tags: [net]
+order: 65
+---
+
+The browser's `fetch()` blocks a response when the server doesn't allow
+cross-origin requests, so many APIs and websites can't be called from your
+app's page. The [`puter.net.fetch()`](/Networking/fetch/) method makes the
+same HTTP and HTTPS requests without that restriction. It takes the same
+arguments as `fetch()` and returns a `Response`.
+
+The first request opens the Puter sign-in window if the user isn't signed in
+yet.
+
+## Fetch a JSON API
+
+To fetch JSON, pass the API's URL, then read the response with `json()`:
+
+```js
+const response = await puter.net.fetch('https://httpbin.org/get');
+const data = await response.json();
+
+console.log(data);
+```
+
+The method returns a `Response` object. For a page or a plain text file, use
+`response.text()` instead.
+
+## Send JSON
+
+To send JSON, pass `method`, `headers` and `body` in the second argument.
+Use `JSON.stringify()` to turn the object into a string and set `Content-Type`
+so the server knows how to read it. Custom headers go in the same object:
+
+```js
+const response = await puter.net.fetch('https://httpbin.org/post', {
+    method: 'POST',
+    headers: {
+        'Content-Type': 'application/json',
+        'Accept': 'application/json',
+        'X-Custom-Header': 'custom-value',
+    },
+    body: JSON.stringify({ message: 'Hello' }),
+});
+
+const data = await response.json();
+
+console.log(data);
+```
+
+This example uses httpbin, which sends back the JSON and headers it received.
+The other request options are listed on
+[`puter.net.fetch()`](/Networking/fetch/).
+
+## Read a Response in Chunks
+
+To process a large response as it arrives, read from `response.body` instead
+of waiting for `text()` or `json()` to collect the whole body:
+
+```js
+const response = await puter.net.fetch('https://httpbin.org/stream-bytes/102400');
+const reader = response.body.getReader();
+
+try {
+    while (true) {
+        const { done, value } = await reader.read();
+        if (done) break;
+
+        console.log(value);
+    }
+} finally {
+    reader.releaseLock();
+}
+```
+
+Each `value` is a `Uint8Array` of bytes. Chunk sizes can vary. Some responses,
+such as those to a `HEAD` request, have no body, so `response.body` is `null`.
+
+## Handle a Failed Request
+
+A request can fail before you get a response, for example when the host cannot
+be reached. Catch the error with `try`/`catch`. HTTP errors such as `404` or
+`500` still return a response, so check `response.ok` before reading the body:
+
+```js
+try {
+    const response = await puter.net.fetch('https://httpbin.org/get');
+    if (!response.ok) {
+        throw new Error(`Request failed (HTTP ${response.status}).`);
+    }
+
+    const data = await response.json();
+    console.log(data);
+} catch (error) {
+    console.error('Could not load the data:', error?.message ?? error);
+}
+```
+
+The `ok` property is `true` for status codes from `200` to `299`. The same
+catch also handles a response that can't be read as JSON.
+
+To try the HTTP error path, replace the URL with
+`https://httpbin.org/status/404`. To try a host that does not exist, use
+`https://does-not-exist.invalid/`.
+
+## Notes
+
+- For an API that needs a secret key, call it from a worker so the key stays
+  out of your app's page. See [keep an API key
+  secret](/recipes/keep-api-key-secret/).