Avatar

@saolsen

14 likes26 public vals
Joined June 7, 2023

View val changes as a diff.

Go to /v/username/valname/version to see a diff between that version and the previous one. For example https://saolsen-changes.web.val.run/v/saolsen/tracing/108

Readme
Fork
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/* @jsxImportSource https://esm.sh/hono/jsx */
import { Hono } from "npm:hono";
import { html } from "npm:hono/html";
import { track } from "https://esm.town/v/saolsen/plausible?v=3";
import {
init,
traced_fetch,
traced_handler,
} from "https://esm.town/v/saolsen/tracing";
init("changes");
const app = new Hono();
app.get("/", (c) => {
return c.html(
<div>
Go to `/v/:user/:val` to see the latest version of a val, or
`/v/:user/:val/:version` to see a specific version. For example,{" "}
<a href="/v/saolsen/tracing/108">/v/saolsen/tracing/108</a>
</div>
);
});
type Val = {
user: string;
val: string;
version: number;
body: string;
};
async function get_val(
user: string,
val: string,
version: number | null = null
): Promise<Val> {
let val_req_url = `https://esm.town/v/${user}/${val}`;
if (version !== null) {
val_req_url = `${val_req_url}?v=${version}`;
}
const val_resp: Response = await traced_fetch(val_req_url);
const val_version = Number(val_resp.url.split("?v=")[1]);
const val_body: string = await val_resp.text();
return {
user,
val,
version: val_version,
body: val_body,
};
}

Opentelemetry Tracing for Vals!

Enables Opentelemetry tracing for vals. Exports two functions, init and traced_handler. init will set up opentelemetry.

For how to use it see this example val.

By default, traces log to the console but if you set HONEYCOMB_API_KEY it'll also push the traces to honeycomb. In the future we can add more export targets for other services. I'm also thinking about making a val to display them.

traced_handler is a wrapper for your handler you can use on an HTTP val. Just pass it your HTTP function and export it as the default and it'll trace every request.

Here's a screenshot of what the honeycomb view of a trace from my connect playing val looks like.

CleanShot 2023-12-22 at 11.51.13@2x.png

Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import "https://deno.land/x/xhr@0.1.0/mod.ts";
import "node:async_hooks";
import { context, propagation, SpanStatusCode, trace } from "https://cdn.skypack.dev/@opentelemetry/api";
import { OTLPTraceExporter } from "https://cdn.skypack.dev/@opentelemetry/exporter-trace-otlp-http";
import { B3Propagator } from "https://cdn.skypack.dev/@opentelemetry/propagator-b3";
import { Resource } from "https://cdn.skypack.dev/@opentelemetry/resources";
import {
ConsoleSpanExporter,
SimpleSpanProcessor,
WebTracerProvider,
} from "https://cdn.skypack.dev/@opentelemetry/sdk-trace-web";
import { SemanticResourceAttributes } from "https://cdn.skypack.dev/@opentelemetry/semantic-conventions";
import { AsyncLocalStorageContextManager } from "npm:@opentelemetry/context-async-hooks";
export function get_tracer() {
return trace.getTracer("valtown");
}
/**
* Initializes opentelemetry tracing.
*/
export function init(service_name: string, console_log: bool = false): undefined {
const provider = new WebTracerProvider({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: service_name,
}),
});
// Log traces to console if console_log is set.
if (console_log) {
provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
}
// Send traces to honeycomb if HONEYCOMB_API_KEY key is set.
const honeycomb_api_key = Deno.env.get("HONEYCOMB_API_KEY");
if (honeycomb_api_key) {
provider.addSpanProcessor(
new SimpleSpanProcessor(
new OTLPTraceExporter({
url: "https://api.honeycomb.io/v1/traces",
headers: {
"x-honeycomb-team": honeycomb_api_key,
},
}),
),
);
}
provider.register({
contextManager: new AsyncLocalStorageContextManager(),
propagator: new B3Propagator(),

Play connect4.

  • Write agents that play connect4.
  • Battle your agents against other agents.
  • It's fun.
Readme
Fork
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/* @jsxImportSource https://esm.sh/hono/jsx */
import { track } from "https://esm.town/v/saolsen/plausible?v=3";
import { init, traced_fetch, traced_handler, traced, get_tracer } from "https://esm.town/v/saolsen/tracing?v=135";
import { trace } from "npm:@opentelemetry/api";
init("connect4_site");
const tracer = get_tracer("connect4_site");
import { blob } from "https://esm.town/v/std/blob";
import { customAlphabet } from "npm:nanoid";
import { z } from "npm:zod";
import { Hono } from "npm:hono";
import { html } from "npm:hono/html";
import { jsxRenderer, useRequestContext } from "npm:hono/jsx-renderer";
import * as connect4 from "https://esm.town/v/saolsen/connect4";
// TODO:
// * htmx loading spinners
const _nanoid = customAlphabet("123456789abcdefghijklmnopqrstuvwxyz", 10);
const MatchId = z.string().startsWith("m_");
type MatchId = z.infer<typeof MatchId>;
function matchid(): MatchId {
return `m_${_nanoid()}`;
}
type MePlayer = {
kind: "me";
};
type AgentPlayer = {
kind: "agent";
name: string;
url: string;
};
type Player = MePlayer | AgentPlayer;
type AgentFetchError = {
kind: "agent_fetch";
error_message: string;
};
type AgentHTTPResponseError = {
kind: "agent_http_response";
status: number;
body: string;
};

Prune a val's versions.

Useful if you want to delete a bunch of versions.

All versions before keep_since that aren't in keep_versions will be deleted!!!

You can run it without passing commit to see a preview of what will happen.

Example

await prune_val("abcdefg", [3,6,8], 12, true);

Could output

Val: untitled_peachTakin, Current Version: 15
Deleting Versions: [ 1, 2, 4, 5, 7, 8, 9, 10, 11 ]
Deleting Version 1
Deleted
Deleting Version 2
Deleted
Deleting Version 4
Deleted
Deleting Version 5
Deleted
Deleting Version 6
Deleted
Deleting Version 7
Version already deleted, skipping
Deleting Version 8
Deleted
Deleting Version 9
Deleted
Deleting Version 10
Deleted
Deleting Version 11
Deleted
Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
export async function prune_val(
val_id: string,
keep_versions: number[],
keep_since: number,
commit = false,
): Promise<void> {
const resp = await fetch(`https://api.val.town/v1/vals/${val_id}`, {
headers: {
Authorization: "Bearer " + Deno.env.get("valtown"),
},
});
if (resp.status !== 200) {
console.error("Error fetching val:", resp);
return;
}
const val = await resp.json();
console.log(`Val: ${val.name}, Current Version: ${val.version}`);
const versions_to_delete = [];
for (let i = 1; i <= val.version; i++) {
if (i >= keep_since) {
break;
}
if (keep_versions.indexOf(i) == -1) {
versions_to_delete.push(i);
}
}
console.log("Deleting Versions:", versions_to_delete);
for (const v of versions_to_delete) {
console.log(`Deleting Version ${v}`);
const resp = await fetch(
`https://api.val.town/v1/vals/${val_id}/versions/${v}`,
{
headers: {
Authorization: "Bearer " + Deno.env.get("valtown"),
},
},
);
switch (resp.status) {
case 200: {
if (commit) {
const resp = await fetch(
`https://api.val.town/v1/vals/${val_id}/versions/${v}`,
{
method: "DELETE",
headers: {
Authorization: "Bearer " + Deno.env.get("valtown"),
},
},

Tiny migrations "framework" that makes using sqlite in vals a little easier.

Not great yet, so far can only run "up" for migrations you haven't run yet or down for all the ones you have run.

See https://www.val.town/v/saolsen/sqlite_migrations_example for usage.

Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { sqlite } from "https://esm.town/v/std/sqlite";
export type Migration = {
name: string;
up: string;
down: string;
};
export type Migrations = {
name: string;
migrations: Migration[];
};
export async function migrate(migrations) {
console.log("Running Migrations");
await sqlite.execute(`
create table if not exists migrations (
id integer primary key autoincrement,
name text not null,
step integer not null
) strict;
`);
let rs = await sqlite.execute({
sql: "select step from migrations where name = :name",
args: { name: migrations.name },
});
let step: number = 0;
if (rs.rows.length === 0) {
await sqlite.execute({
sql: "insert into migrations(name, step) values (:name, :step)",
args: { name: migrations.name, step: 0 },
});
} else {
step = Number(rs.rows[0][0]);
}
let batch = [];
let i = 0;
for (i = 0; i < migrations.migrations.length; i++) {
let migration = migrations.migrations[i];
let migration_step = Number(i) + 1;
if (migration_step > step) {
batch.push(migration.up);
console.log(" ", migration_step, migration.name);
}
}
batch.push({
sql: `update migrations set step = :step where name = :name`,
args: { name: migrations.name, step: i },
});
await sqlite.batch(batch);
1
2
3
4
5
6
7
8
9
10
11
12
import { expect, prettify, Test } from "npm:tiny-jest";
const { it, run, title } = new Test("basic");
it("2+2=4", () => {
expect(2 + 2).toBe(4);
});
it("1+2=4", async () => {
expect(1 + 2).toBe(4);
});
run().then(prettify);

Sudoku Solver

Solves Sudoku puzzles via backtracking search.

Pass in a 9x9 Sudoku puzzle array (of arrays) with 0's for empty slots. Returns a solved puzzle or null if the puzzle can't be solved.

Example example_val

import { print, solve, type Sudoku } from "https://esm.town/v/saolsen/sudoku_solver";

const sudoku: Sudoku = [
  [0, 0, 4, 5, 0, 0, 0, 8, 0],
  [3, 5, 7, 8, 0, 4, 6, 1, 0],
  [8, 1, 2, 0, 0, 6, 5, 0, 0],
  [0, 4, 0, 3, 8, 0, 7, 0, 0],
  [0, 0, 0, 0, 6, 0, 0, 9, 8],
  [0, 2, 8, 0, 5, 9, 0, 0, 0],
  [0, 6, 0, 0, 0, 0, 8, 3, 0],
  [5, 0, 1, 6, 7, 0, 9, 4, 2],
  [0, 7, 0, 0, 0, 0, 1, 0, 0],
];

const solved = solve(sudoku);
print(solved);

prints

6  9  4  5  1  7  2  8  3  
3  5  7  8  2  4  6  1  9  
8  1  2  9  3  6  5  7  4  
9  4  6  3  8  1  7  2  5  
1  3  5  7  6  2  4  9  8  
7  2  8  4  5  9  3  6  1  
2  6  9  1  4  5  8  3  7  
5  8  1  6  7  3  9  4  2  
4  7  3  2  9  8  1  5  6 
Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// Brute Force Sudoku Solver
import { z } from "npm:zod";
const ROWS = 9;
const COLS = 9;
const Slot = z.number().gte(0).lte(9);
const Sudoku = z.array(z.array(Slot).length(COLS)).length(ROWS);
export type Sudoku = z.infer<typeof Sudoku>;
export function print(sudoku: Sudoku) {
for (let row = 0; row < ROWS; row++) {
let line = "";
for (let col = 0; col < COLS; col++) {
line += sudoku[row][col] + " ";
}
console.log(line);
}
}
type Index = { x: number; y: number };
function next_i(i: Index): Index | null {
if (i.x === COLS - 1 && i.y === ROWS - 1) {
return null;
}
if (i.x < COLS - 1) {
return { x: i.x + 1, y: i.y };
}
return { x: 0, y: i.y + 1 };
}
function valid_row(sudoku: Sudoku, i: Index): boolean {
const seen = new Set();
for (let col = 0; col < COLS; col++) {
const val = sudoku[i.y][col];
if (val !== 0) {
if (seen.has(val)) {
return false;
}
seen.add(val);
}
}
return true;
}
function valid_col(sudoku: Sudoku, i: Index): boolean {
const seen = new Set();
for (let row = 0; row < ROWS; row++) {
const val = sudoku[row][i.x];

This is a wrapper of the val town std sqlite library that adds tracing via https://www.val.town/v/saolsen/tracing.

Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { SpanStatusCode } from "https://cdn.skypack.dev/@opentelemetry/api";
import { type ResultSet, type TransactionMode } from "npm:@libsql/client";
import { InStatement, sqlite as std_sqlite } from "https://esm.town/v/std/sqlite?v=4";
import { get_tracer } from "https://esm.town/v/saolsen/tracing?v=136";
async function traced_execute(statement: InStatement): Promise<ResultSet> {
return await get_tracer().startActiveSpan(`sqlite:execute`, async (span) => {
if (span.isRecording()) {
if (typeof statement === "string") {
span.setAttributes({
"sqlite.statement": statement,
"sqlite.args": [],
});
} else {
span.setAttributes({
"sqlite.statement": statement.sql,
"sqlite.args": statement.args,
});
}
}
try {
const result = await std_sqlite.execute(statement);
if (span.isRecording()) {
span.setStatus({ code: SpanStatusCode.OK });
}
return result;
} catch (error) {
if (span.isRecording()) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
});
}
throw new Error(error);
} finally {
span.end();
}
});
}
async function traced_batch(statements: InStatement[], mode?: TransactionMode): Promise<ResultSet[]> {
return await get_tracer().startActiveSpan(`sqlite:batch`, async (span) => {
if (span.isRecording()) {
span.setAttributes({
"sqlite.statements": JSON.stringify(statements),
});
}
try {

Upstash qstash client for vals.

Lets you publish and receive qstash messages in vals.

To use, set up qstash on Upstash and set the environment variables.

  • QSTASH_TOKEN
  • QSTASH_CURRENT_SIGNING_KEY
  • QSTASH_NEXT_SIGNING_KEY

Calls are also traced via https://www.val.town/v/saolsen/tracing so if you use that library you will see linked spans between the publish call and the receive call.

For an example of how to use it see https://www.val.town/v/saolsen/try_qstash_publish and https://www.val.town/v/saolsen/try_qstash_receive.

Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import {
context,
propagation,
SpanStatusCode,
trace,
} from "https://cdn.skypack.dev/@opentelemetry/api";
import { Client, Receiver } from "npm:@upstash/qstash";
import { get_tracer, traced } from "https://esm.town/v/saolsen/tracing?v=136";
const client = new Client({
token: Deno.env.get("QSTASH_TOKEN")!,
});
export type PublishOpts = { url?: string; topic?: string };
export async function publish(
body: object,
opts: PublishOpts
): Promise<{ messageId: string }> {
if ((opts.url && opts.topic) || (!opts.url && !opts.topic)) {
throw new Error("Must set one of url or topic.");
}
return await get_tracer().startActiveSpan(`qstash:publish`, async (span) => {
try {
const prop_output: { b3: string } = { b3: "" };
propagation.inject(context.active(), prop_output);
const result = await client.publishJSON({
body,
headers: prop_output,
...opts,
});
if (span.isRecording()) {
span.setAttributes({
"qstash:messageId": result.messageId,
});
span.setStatus({ code: SpanStatusCode.OK });
}
return result;
} catch (error) {
if (span.isRecording()) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
});
}
throw new Error(error);
} finally {
span.end();
}
});

Upstash redis client for vals.

Creates a global redis instance and exposes functions to use it.

To use, create a redis database on Upstash and set the environment variables.

  • UPSTASH_REDIS_REST_URL
  • UPSTASH_REDIS_REST_TOKEN

Val.town runs in ohio so the best region to pick for the redis instance is probably us-east-1.

Calls are also traced via https://www.val.town/v/saolsen/tracing so if you use that library you will see timed spans for any redis calls.

For an example of how to use it see https://www.val.town/v/saolsen/try_redis

Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import { SpanStatusCode } from "https://cdn.skypack.dev/@opentelemetry/api";
import { Redis } from "https://deno.land/x/upstash_redis@v1.14.0/mod.ts";
import { get_tracer } from "https://esm.town/v/saolsen/tracing?v=136";
const redis = new Redis({
url: Deno.env.get("UPSTASH_REDIS_REST_URL"),
token: Deno.env.get("UPSTASH_REDIS_REST_TOKEN"),
});
const traced = <F extends (...args: any[]) => any>(
name: string,
fn: F,
) => {
return async (...args: Parameters<F>): Promise<Awaited<ReturnType<F>>> => {
console.log(fn);
const key = args[0];
return await get_tracer().startActiveSpan(`redis:${name}`, async (span) => {
if (span.isRecording()) {
span.setAttributes({
"redis.key": key,
});
}
try {
const result = await fn(...args);
if (span.isRecording()) {
span.setStatus({ code: SpanStatusCode.OK });
}
return result;
} catch (error) {
if (span.isRecording()) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
});
}
throw new Error(error);
} finally {
span.end();
}
});
};
};
export const set = traced("set", redis.set);
export const get = traced("get", redis.get);