<html lang="en">
<head></head>
<body>

<form id="mainForm" method="post" action="https://stackblitz.com/run" target="_self">
<input type="hidden" name="project[files][.gitignore]" value=".actorcore
node_modules">
<input type="hidden" name="project[files][README.md]" value="# Smoke Test for RivetKit

Example project demonstrating a simple getOrCreate smoke test with [RivetKit](https://rivetkit.org).

[Learn More →](https://github.com/rivet-dev/rivetkit)

[Discord](https://rivet.dev/discord) — [Documentation](https://rivetkit.org) — [Issues](https://github.com/rivet-dev/rivetkit/issues)

## Getting Started

### Prerequisites

- Node.js

### Installation

```sh
git clone https://github.com/rivet-dev/rivetkit
cd rivetkit/examples/smoke-test
npm install
```

### Development

```sh
npm run dev
```

Run the smoke test to exercise multiple actor creations:

```sh
npm run smoke
```

Set `TOTAL_ACTOR_COUNT` and `SPAWN_ACTOR_INTERVAL` environment variables to adjust the workload.

## License

Apache 2.0
">
<input type="hidden" name="project[files][package.json]" value="{&quot;name&quot;:&quot;example-smoke-test&quot;,&quot;version&quot;:&quot;2.0.20&quot;,&quot;private&quot;:true,&quot;type&quot;:&quot;module&quot;,&quot;scripts&quot;:{&quot;dev&quot;:&quot;tsx src/server/server.ts&quot;,&quot;check-types&quot;:&quot;tsc --noEmit&quot;,&quot;smoke&quot;:&quot;tsx src/smoke-test/index.ts&quot;,&quot;connect&quot;:&quot;tsx scripts/connect.ts&quot;},&quot;devDependencies&quot;:{&quot;rivetkit&quot;:&quot;https://pkg.pr.new/rivet-dev/rivetkit/rivetkit@54999214372f9ecb3f4251a3be45c7a0eb8dfacf&quot;,&quot;@types/node&quot;:&quot;^22.13.9&quot;,&quot;tsx&quot;:&quot;^3.12.7&quot;,&quot;typescript&quot;:&quot;^5.7.3&quot;},&quot;stableVersion&quot;:&quot;0.8.0&quot;}">
<input type="hidden" name="project[files][tsconfig.json]" value="{
  &quot;compilerOptions&quot;: {
    /* Visit https://aka.ms/tsconfig.json to read more about this file */

    /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
    &quot;target&quot;: &quot;esnext&quot;,
    /* Specify a set of bundled library declaration files that describe the target runtime environment. */
    &quot;lib&quot;: [&quot;esnext&quot;],
    /* Specify what JSX code is generated. */
    &quot;jsx&quot;: &quot;react-jsx&quot;,

    /* Specify what module code is generated. */
    &quot;module&quot;: &quot;esnext&quot;,
    /* Specify how TypeScript looks up a file from a given module specifier. */
    &quot;moduleResolution&quot;: &quot;bundler&quot;,
    /* Specify type package names to be included without being referenced in a source file. */
    &quot;types&quot;: [&quot;node&quot;],
    /* Enable importing .json files */
    &quot;resolveJsonModule&quot;: true,

    /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
    &quot;allowJs&quot;: true,
    /* Enable error reporting in type-checked JavaScript files. */
    &quot;checkJs&quot;: false,

    /* Disable emitting files from a compilation. */
    &quot;noEmit&quot;: true,

    /* Ensure that each file can be safely transpiled without relying on other imports. */
    &quot;isolatedModules&quot;: true,
    /* Allow &#39;import x from y&#39; when a module doesn&#39;t have a default export. */
    &quot;allowSyntheticDefaultImports&quot;: true,
    /* Ensure that casing is correct in imports. */
    &quot;forceConsistentCasingInFileNames&quot;: true,

    /* Enable all strict type-checking options. */
    &quot;strict&quot;: true,

    /* Skip type checking all .d.ts files. */
    &quot;skipLibCheck&quot;: true
  },
  &quot;include&quot;: [&quot;src/**/*.ts&quot;, &quot;tests/**/*.ts&quot;]
}
">
<input type="hidden" name="project[files][turbo.json]" value="{
  &quot;$schema&quot;: &quot;https://turbo.build/schema.json&quot;,
  &quot;extends&quot;: [&quot;//&quot;]
}
">
<input type="hidden" name="project[files][scripts/connect.ts]" value="import { createClient } from &quot;rivetkit/client&quot;;
import type { Registry } from &quot;../src/registry&quot;;

async function main() {
	const client = createClient&lt;Registry&gt;(&quot;http://localhost:6420&quot;);

	const counter = client.counter.getOrCreate().connect();

	counter.on(&quot;newCount&quot;, (count: number) =&gt; console.log(&quot;Event:&quot;, count));

	for (let i = 0; i &lt; 5; i++) {
		const out = await counter.increment(5);
		console.log(&quot;RPC:&quot;, out);

		await new Promise((resolve) =&gt; setTimeout(resolve, 1000));
	}

	await new Promise((resolve) =&gt; setTimeout(resolve, 2000));
	await counter.dispose();

	await new Promise((resolve) =&gt; setTimeout(resolve, 200));

	const counter2 = client.counter.getOrCreate().connect();

	counter2.on(&quot;newCount&quot;, (count: number) =&gt; console.log(&quot;Event:&quot;, count));

	for (let i = 0; i &lt; 5; i++) {
		const out = await counter2.increment(5);
		console.log(&quot;RPC:&quot;, out);

		await new Promise((resolve) =&gt; setTimeout(resolve, 1000));
	}

	await new Promise((resolve) =&gt; setTimeout(resolve, 2000));
	await counter2.dispose();
}

main();
">
<input type="hidden" name="project[files][src/server/registry.ts]" value="import { actor, setup } from &quot;rivetkit&quot;;

const counter = actor({
	options: {
		sleepTimeout: 500,
	},
	state: {
		count: 0,
	},
	actions: {
		increment: (c, x: number) =&gt; {
			c.state.count += x;
			c.broadcast(&quot;newCount&quot;, c.state.count);
			return c.state.count;
		},
		getCount: (c) =&gt; {
			return c.state.count;
		},
	},
});

export const registry = setup({
	use: { counter },
});

export type Registry = typeof registry;
">
<input type="hidden" name="project[files][src/server/server.ts]" value="import { registry } from &quot;./registry&quot;;

registry.start({
	// defaultServerPort: 8080,
	// runnerKind: &quot;serverless&quot;,
	// autoConfigureServerless: true,
});
">
<input type="hidden" name="project[files][src/smoke-test/index.ts]" value="import { randomUUID } from &quot;node:crypto&quot;;
import { createClient } from &quot;rivetkit/client&quot;;
import type { Registry } from &quot;../server/registry&quot;;
import { type SmokeTestError, spawnActor } from &quot;./spawn-actor&quot;;

function parseEnvInt(value: string | undefined, fallback: number) {
	const parsed = Number(value);
	if (!Number.isFinite(parsed) || parsed &lt;= 0) {
		return fallback;
	}
	return Math.floor(parsed);
}

const RUN_DURATION = parseEnvInt(process.env.RUN_DURATION, 10_000);
const SPAWN_ACTOR_INTERVAL = parseEnvInt(process.env.SPAWN_ACTOR_INTERVAL, 10);
const TOTAL_ACTOR_COUNT = Math.ceil(RUN_DURATION / SPAWN_ACTOR_INTERVAL);
const PROGRESS_LOG_INTERVAL_MS = 250;

type DurationStats = {
	average: number;
	median: number;
	min: number;
	max: number;
};

async function delay(ms: number) {
	return new Promise&lt;void&gt;((resolve) =&gt; setTimeout(resolve, ms));
}

function calculateDurationStats(durations: number[]): DurationStats {
	if (durations.length === 0) {
		return { average: 0, median: 0, min: 0, max: 0 };
	}

	const sorted = [...durations].sort((left, right) =&gt; left - right);
	const count = sorted.length;
	const sum = sorted.reduce((runningSum, duration) =&gt; runningSum + duration, 0);
	const average = sum / count;
	const min = sorted[0];
	const max = sorted[count - 1];
	const median =
		count % 2 === 0
			? (sorted[count / 2 - 1] + sorted[count / 2]) / 2
			: sorted[Math.floor(count / 2)];

	return { average, median, min, max };
}

function logProgress({
	totalActorCount,
	startedCount,
	successCount,
	failureCount,
	iterationDurations,
}: {
	totalActorCount: number;
	startedCount: number;
	successCount: number;
	failureCount: number;
	iterationDurations: number[];
}) {
	const stats = calculateDurationStats(iterationDurations);
	const remainingCount = totalActorCount - startedCount;
	const pendingCount = Math.max(
		0,
		startedCount - (successCount + failureCount),
	);
	console.log(
		`progress: success=${successCount}, failures=${failureCount}, pending=${pendingCount}, remaining=${remainingCount}, duration(ms): avg=${stats.average.toFixed(2)}, median=${stats.median.toFixed(2)}, min=${stats.min.toFixed(2)}, max=${stats.max.toFixed(2)}`,
	);
}

async function main() {
	const client = createClient&lt;Registry&gt;(&quot;http://localhost:6420&quot;);
	const testId = randomUUID();
	const errors: SmokeTestError[] = [];
	let successCount = 0;
	let failureCount = 0;
	let startedCount = 0;
	const iterationDurations: number[] = [];
	const logProgressTick = () =&gt;
		logProgress({
			totalActorCount: TOTAL_ACTOR_COUNT,
			startedCount,
			successCount,
			failureCount,
			iterationDurations,
		});

	console.log(
		`starting smoke test (run duration: ${RUN_DURATION}ms, interval: ${SPAWN_ACTOR_INTERVAL}ms, expected actors: ${TOTAL_ACTOR_COUNT}, test id: ${testId})`,
	);

	const pendingActors: Promise&lt;void&gt;[] = [];
	const progressInterval = setInterval(
		logProgressTick,
		PROGRESS_LOG_INTERVAL_MS,
	);
	logProgressTick();

	try {
		for (let index = 0; index &lt; TOTAL_ACTOR_COUNT; index++) {
			startedCount += 1;
			pendingActors.push(
				spawnActor({
					client,
					index,
					testId,
					errors,
					iterationDurations,
					onSuccess: () =&gt; {
						successCount += 1;
					},
					onFailure: () =&gt; {
						failureCount += 1;
					},
				}),
			);
			if (index &lt; TOTAL_ACTOR_COUNT - 1) {
				await delay(SPAWN_ACTOR_INTERVAL);
			}
		}

		await Promise.all(pendingActors);
	} finally {
		clearInterval(progressInterval);
	}

	logProgressTick();

	const finalStats = calculateDurationStats(iterationDurations);
	console.log(
		`iteration duration stats (ms): avg=${finalStats.average.toFixed(2)}, median=${finalStats.median.toFixed(2)}, min=${finalStats.min.toFixed(2)}, max=${finalStats.max.toFixed(2)}`,
	);

	if (errors.length &gt; 0) {
		console.error(`completed with ${errors.length} error(s)`);
		errors.forEach(({ index, error }) =&gt; {
			console.error(`[${index}] captured error`, error);
		});
		process.exitCode = 1;
		return;
	}

	console.log(&quot;smoke test completed successfully&quot;);
}

main().catch((error) =&gt; {
	console.error(&quot;fatal error&quot;, error);
	process.exit(1);
});
">
<input type="hidden" name="project[files][src/smoke-test/spawn-actor.ts]" value="import { performance } from &quot;node:perf_hooks&quot;;
import type { createClient } from &quot;rivetkit/client&quot;;
import type { Registry } from &quot;../server/registry&quot;;

export type SmokeTestError = {
	index: number;
	error: unknown;
};

export type RegistryClient = ReturnType&lt;typeof createClient&lt;Registry&gt;&gt;;

export type SpawnActorOptions = {
	client: RegistryClient;
	index: number;
	testId: string;
	errors: SmokeTestError[];
	iterationDurations: number[];
	onSuccess: () =&gt; void;
	onFailure: () =&gt; void;
};

export async function spawnActor({
	client,
	index,
	testId,
	errors,
	iterationDurations,
	onSuccess,
	onFailure,
}: SpawnActorOptions): Promise&lt;void&gt; {
	const iterationStart = performance.now();
	let succeeded = false;

	try {
		const key = [&quot;test&quot;, testId, index.toString()];
		const counter = client.counter.getOrCreate(key).connect();
		await counter.increment(1);
		await counter.dispose();

		// Immediately reconnect
		const counter2 = client.counter.getOrCreate(key).connect();
		await counter2.increment(1);
		await counter2.dispose();

		// Wait for actor to sleep
		await new Promise((res) =&gt; setTimeout(res, 1000));

		// Reconnect after sleep
		const counter3 = client.counter.getOrCreate(key).connect();
		await counter3.increment(1);
		await counter3.dispose();

		succeeded = true;
		onSuccess();
	} catch (error) {
		errors.push({ index, error });
		onFailure();
	}

	if (succeeded) {
		const iterationEnd = performance.now();
		const iterationDuration = iterationEnd - iterationStart;
		iterationDurations.push(iterationDuration);
	}
}
">
<input type="hidden" name="project[description]" value="generated by https://pkg.pr.new">
<input type="hidden" name="project[template]" value="node">
<input type="hidden" name="project[title]" value="example-smoke-test">
</form>
<script>document.getElementById("mainForm").submit();</script>

</body></html>