Skip to content
This repository has been archived by the owner on Jul 5, 2024. It is now read-only.

feat: add example project #34

Merged
merged 18 commits into from
Oct 6, 2023
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
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ coverage
lib
node_modules
pnpm-lock.yaml
pnpm-workspace.yaml
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ coverage/
lib/
pnpm-lock.yaml
/CHANGELOG.md
pnpm-workspace.yaml
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ type Obj = typeof obj;

### Streaming `Promise`s and `AsyncIterable`s

> See test files called `deserializeAsync.test.ts`
- See example in [`./examples/async`](./examples/async)
- [Test it on StackBlitz](https://stackblitz.com/github/KATT/tupleson/tree/main/examples/async?file=src/server.ts&file=src/client.ts&view=editor)

## Extend with a custom serializer

Expand Down
27 changes: 27 additions & 0 deletions examples/async/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@examples/minimal",
"version": "10.38.5",
"private": true,
"description": "An example project for tupleson",
"license": "MIT",
"module": "module",
"workspaces": [
"client",
"server"
],
"scripts": {
"build": "tsc",
"dev:server": "tsx watch src/server",
"dev:client": "tsx watch src/client",
"dev": "run-p dev:* --print-label",
"lint": "eslint --ext \".js,.ts,.tsx\" --report-unused-disable-directives */*.ts"
},
"devDependencies": {
"@types/node": "^18.16.16",
"npm-run-all": "^4.1.5",
"tsx": "^3.12.7",
"tupleson": "latest",
"typescript": "^5.1.3",
"wait-port": "^1.0.1"
}
}
54 changes: 54 additions & 0 deletions examples/async/src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import waitPort from "wait-port";

import type { ResponseShape } from "./server.js";

import { mapIterable, readableStreamToAsyncIterable } from "./iteratorUtils.js";
import { tsonAsync } from "./shared.js";

async function main() {
// do a streamed fetch request
const port = 3000;
await waitPort({ port });

const response = await fetch(`http://localhost:${port}`);

if (!response.body) {
throw new Error("Response body is empty");
}

const textDecoder = new TextDecoder();

// convert the response body to an async iterable
const stringIterator = mapIterable(
readableStreamToAsyncIterable(response.body),
(v) => textDecoder.decode(v),
);

// ✨ ✨ ✨ ✨ parse the response body stream ✨ ✨ ✨ ✨ ✨
const parsedUntyped = await tsonAsync.parse(stringIterator);
const output = parsedUntyped as ResponseShape;

// we can now use the output as a normal object
console.log({ output });

const printBigInts = async () => {
for await (const value of output.bigints) {
console.log(`Received bigint:`, value);
}
};

const printNumbers = async () => {
for await (const value of output.numbers) {
console.log(`Received number:`, value);
}
};

await Promise.all([printBigInts(), printNumbers()]);

console.log("✅ Output ended");
}

main().catch((err) => {
console.error(err);
throw err;
});
33 changes: 33 additions & 0 deletions examples/async/src/iteratorUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export async function* readableStreamToAsyncIterable<T>(
stream: ReadableStream<T>,
): AsyncIterable<T> {
// Get a lock on the stream
const reader = stream.getReader();

try {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
while (true) {
// Read from the stream
const result = await reader.read();

// Exit if we're done
if (result.done) {
return;
}

// Else yield the chunk
yield result.value;
}
} finally {
reader.releaseLock();
}
}

export async function* mapIterable<T, TValue>(
iterable: AsyncIterable<T>,
fn: (v: T) => TValue,
): AsyncIterable<TValue> {
for await (const value of iterable) {
yield fn(value);
}
}
71 changes: 71 additions & 0 deletions examples/async/src/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import http from "node:http";

import { tsonAsync } from "./shared.js";

const randomNumber = (min: number, max: number) => {
return Math.floor(Math.random() * (max - min + 1) + min);
};

export function getResponseShape() {
async function* bigintGenerator() {
const iterate = new Array(10).fill(0).map((_, i) => BigInt(i));
for (const number of iterate) {
await new Promise((resolve) => setTimeout(resolve, randomNumber(1, 400)));
yield number;
}
}

async function* numberGenerator() {
const iterate = new Array(10).fill(0).map((_, i) => i);
for (const number of iterate) {
await new Promise((resolve) => setTimeout(resolve, randomNumber(1, 400)));
yield number;
}
}

return {
bigints: bigintGenerator(),
foo: "bar",
numbers: numberGenerator(),
// promise: Promise.resolve(42),
promise: new Promise<number>((resolve) =>
setTimeout(() => {
resolve(42);
}, 1),
),
rejectedPromise: new Promise<number>((_, reject) =>
setTimeout(() => {
reject(new Error("Rejected promise"));
}, 1),
),
};
}

export type ResponseShape = ReturnType<typeof getResponseShape>;
async function handleRequest(
req: http.IncomingMessage,
res: http.ServerResponse,
) {
res.writeHead(200, { "Content-Type": "application/json" });

const obj = getResponseShape();

for await (const chunk of tsonAsync.stringify(obj)) {
res.write(chunk);
}
}

const server = http.createServer(
(req: http.IncomingMessage, res: http.ServerResponse) => {
handleRequest(req, res).catch((err) => {
console.error(err);
res.writeHead(500, { "Content-Type": "text/plain" });
res.end("Internal Server Error\n");
});
},
);

const port = 3000;
server.listen(port);

console.log(`Server running at http://localhost:${port}`);
10 changes: 10 additions & 0 deletions examples/async/src/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {
createTsonAsync,
tsonAsyncIterator,
tsonBigint,
tsonPromise,
} from "tupleson";

export const tsonAsync = createTsonAsync({
types: [tsonPromise, tsonAsyncIterator, tsonBigint],
});
11 changes: 11 additions & 0 deletions examples/async/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"outDir": "dist",
"strict": true,
"target": "esnext"
},
"include": ["src"]
}
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"fix": "pnpm lint --fix && pnpm format:write",
"format": "prettier \"**/*\" --ignore-unknown",
"format:write": "pnpm format --write",
"postinstall": "pnpm run build",
"lint": "eslint . .*js --max-warnings 0 --report-unused-disable-directives",
"lint:md": "markdownlint \"**/*.md\" \".github/**/*.md\" --rules sentences-per-line",
"lint:package-json": "npmPkgJsonLint .",
Expand Down Expand Up @@ -79,5 +80,10 @@
},
"publishConfig": {
"provenance": true
},
"pnpm": {
"overrides": {
"@trpc/server": "link:./"
}
}
}
Loading