Avatar

@saolsen

22 likes44 public vals
Joined June 7, 2023

Telemetry For Vals.

Telemetry is a library that lets you trace val town executions with opentelemetry. All traces are stored in val.town sqlite and there is an integrated trace viewer to see them.

image.png

Quickstart

Instrument an http val like this.

import {
  init,
  tracedHandler,
} from "https://esm.town/v/saolsen/telemetry";

// Set up tracing by passing in `import.meta.url`.
// be sure to await it!!!
await init(import.meta.url);

async function handler(req: Request): Promise<Response> {
  // whatever else you do.
  return 
  
}

export default tracedHandler(handler);

This will instrument the http val and trace every request. Too add additional traces see this widgets example.

Then, too see your traces create another http val like this.

import { traceViewer } from "https://esm.town/v/saolsen/telemetry";
export default traceViewer;

This val will serve a UI that lets you browse traces. For example, you can see my UI here.

Tracing

By wrapping your http handler in tracedHandler all your val executions will be traced. You can add additional traces by using the helpers.

  • trace lets you trace a block of syncronous code.
import { trace } from "https://esm.town/v/saolsen/telemetry";

trace("traced block", () => {
    // do something
});
  • traceAsync lets you trace a block of async code.
import { traceAsync } from "https://esm.town/v/saolsen/telemetry";

await traceAsync("traced block", await () => {
    // await doSomething();
});
  • traced wraps an async function in tracing.
import { traceAsync } from "https://esm.town/v/saolsen/telemetry";

const myTracedFunction: () => Promise<string> = traced(
  "myTracedFunction",
  async () => {
    // await sleep(100);
    return "something";
  },
);
  • fetch is a traced version of the builtin fetch function that traces the request. Just import it and use it like you would use fetch.

  • sqlite is a traced version of the val town sqlite client. Just import it and use it like you would use https://www.val.town/v/std/sqlite

  • attribute adds an attribute to the current span, which you can see in the UI.

  • event adds an event to the current span, which you can see in the UI.

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/** @jsxImportSource npm:hono@4.2.2/jsx */
import "https://deno.land/x/xhr@0.1.0/mod.ts";
import "node:async_hooks";
import {
AttributeValue,
context,
propagation,
Span,
SpanStatusCode,
trace as otelTrace,
Tracer,
} from "npm:@opentelemetry/api";
import { ReadableSpan } from "npm:@opentelemetry/sdk-trace-base";
import {
ExportResult,
ExportResultCode,
hrTimeToMicroseconds,
} from "npm:@opentelemetry/core";
import { B3Propagator } from "npm:@opentelemetry/propagator-b3";
import { Resource } from "npm:@opentelemetry/resources";
import {
SimpleSpanProcessor,
WebTracerProvider,
} from "npm:@opentelemetry/sdk-trace-web";
import { SEMRESATTRS_SERVICE_NAME } from "npm:@opentelemetry/semantic-conventions";
import { AsyncLocalStorageContextManager } from "npm:@opentelemetry/context-async-hooks";
import { and, desc, eq, isNull, sql } from "npm:drizzle-orm@0.30.7";
import {
index,
integer,
primaryKey,
sqliteTable,
text,
} from "npm:drizzle-orm@0.30.7/sqlite-core";
import { drizzle } from "npm:drizzle-orm@0.30.7/libsql";
import { BatchItem } from "npm:drizzle-orm@0.30.7/batch";
import { Hono } from "npm:hono@4.2.2";
import { Child, FC } from "npm:hono@4.2.2/jsx";
import { jsxRenderer, useRequestContext } from "npm:hono@4.2.2/jsx-renderer";
import { html } from "npm:hono@4.2.2/html";
import { type TransactionMode } from "npm:@libsql/client";
import {
InStatement,
ResultSet,
sqlite as std_sqlite,
} from "https://esm.town/v/std/sqlite";
import { extractValInfo } from "https://esm.town/v/pomdtr/extractValInfo";
const tracing_spans = sqliteTable(
"tracing_spans",
{
span_id: text("span_id").notNull().primaryKey(),
trace_id: text("trace_id").notNull(),
parent_span_id: text("parent_span_id"),
service: text("service").notNull(),
name: text("name").notNull(),
timestamp: integer("timestamp", { mode: "timestamp_ms" }).notNull(),
duration: integer("duration").notNull(),
status_code: integer("status_code"),
},
(table) => {
return {
spansTraceIdIdx: index("tracing_spans_trace_id").on(
table.trace_id,
),
spansServiceIdx: index("tracing_spans_service").on(
table.service,
),
spansTimestampIdx: index("tracing_spans_timestamp").on(
table.timestamp,
),
};
},
);
type InsertSpan = typeof tracing_spans.$inferInsert;
type SelectSpan = typeof tracing_spans.$inferSelect;
const tracing_span_attributes = sqliteTable(
"tracing_span_attributes",
{
span_id: text("span_id").notNull().references(
() => tracing_spans.span_id,
{ onDelete: "cascade" },
),
name: text("name").notNull(),
value: text("value", { mode: "json" }).notNull(),
},
(table) => {
return {
pk: primaryKey({ columns: [table.span_id, table.name] }),
spanAttributesSpanIdIdx: index("tracing_span_attributes_span_id").on(
table.span_id,
),
spanAttributesNameIdx: index("tracing_span_attributes_name").on(
table.name,

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
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/* @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,
};
}
function show_val(
latest_version: number,
val: Val,
prev_val: Val | null = null
): HtmlEscapedString {
return (
<html>
<head>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
></meta>
<link
rel="stylesheet"
data-name="vs/editor/editor.main"
href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.20.0/min/vs/editor/editor.main.min.css"
></link>
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.26.1/min/vs/loader.min.js"></script>
</head>
<body>
<div>
{prev_val !== null && (
<a href={`/v/${val.user}/${val.val}/${prev_val.version}`}>
{"<"} v{prev_val.version}
</a>
)}
<strong style="margin: 0 1em">
{val.user}/{val.val} v{val.version}
</strong>
{latest_version !== val.version && (
<a href={`/v/${val.user}/${val.val}/${val.version + 1}`}>
v{val.version + 1} {">"}
</a>
)}
<div>
<div id="container"></div>
</div>
</div>
{html`
<script type="module">
function unescape(str) {
return str.replace(
/&amp;|&lt;|&gt;|&#39;|&quot;/g,
(tag) =>
({
"&amp;": "&",
"&lt;": "<",
"&gt;": ">",
"&#39;": "'",

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
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(),
});
}
/**
* Wrapper for fetch that traces the request. It also passes a propagation header
* so if you are calling another val that uses traced_handler their traces will
* be tied together.
*/
export async function traced_fetch(
input: string | URL,
init?: RequestInit,
): Promise<Response> {
return await get_tracer().startActiveSpan(`fetch`, async (span) => {
const prop_output: { b3: string } = { b3: "" };
propagation.inject(context.active(), prop_output);
try {
const resp: Response = await fetch(input, {
...init,
headers: {
b3: prop_output.b3,
...(init?.headers ?? {}),
},
});
span.setAttributes({
"http.url": resp.url,
"response.status_code": resp.status,
});
if (resp.ok && resp.status >= 200 && resp.status < 400) {
span.setStatus({ code: SpanStatusCode.OK });
} else {
span.setStatus({
code: SpanStatusCode.ERROR,
message: await resp.clone().text(),
});
}
return resp;
} catch (error) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
});
throw new Error(error);
} finally {
span.end();
}
});
}
/**
* Wrapper to trace a function.

Play connect4.

  • Write agents that play connect4.
  • Battle your agents against other agents.
  • It's fun.
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/* @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;
};
type AgentInvalidActionJsonError = {
kind: "agent_invalid_json";
body: string;
parse_error: string;
};
type AgentInvalidActionError = {
kind: "agent_invalid_action";
action: connect4.Action;
error: string;
};
type Error =
| AgentFetchError
| AgentHTTPResponseError
| AgentInvalidActionJsonError
| AgentInvalidActionError;
type ErrorStatus = {
status: "error";
error: Error;
};
type StatusOrError = connect4.Status | ErrorStatus;
type Turn = {
number: number;
status: StatusOrError;
player: number | null;
column: number | null;
state: connect4.State;
};
type Match = {
id: MatchId;
players: [Player, Player];
turns: Turn[];
};
async function get_match(id: MatchId): Promise<Match | null> {
const span = tracer.startSpan("get_match");
span.setAttribute("match_id", id);
const match = await blob.getJSON(`connect4_matches/${id}`);
span.end();
if (match === undefined) {
return null;
}
return match;
}
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/** @jsxImportSource npm:hono@3/jsx */
import { Buffer } from "https://deno.land/std/io/buffer.ts";
import { Hono } from "npm:hono";
import { html } from "npm:hono/html";
import type { Child, FC } from "npm:hono/jsx";
import { jsxRenderer, useRequestContext } from "npm:hono/jsx-renderer";
import { gzip } from "npm:pako";
const app = new Hono();
function createTorus(radius = 1, tube = 0.4, radialSegments = 12, tubularSegments = 48, arc = Math.PI * 2) {
const indicesArray = [];
const positionArray = [];
const uvArray = [];
const vertex = [0, 0, 0];
// generate positions and uvs
for (let j = 0; j <= radialSegments; j++) {
for (let i = 0; i <= tubularSegments; i++) {
const u = (i / tubularSegments) * arc;
const v = (j / radialSegments) * Math.PI * 2;
// position
vertex[0] = (radius + tube * Math.cos(v)) * Math.cos(u);
vertex[1] = (radius + tube * Math.cos(v)) * Math.sin(u);
vertex[2] = tube * Math.sin(v);
positionArray.push(vertex[0], vertex[1], vertex[2]);
// uv
uvArray.push(i / tubularSegments);
uvArray.push(j / radialSegments);
}
}
// generate indices
for (let j = 1; j <= radialSegments; j++) {
for (let i = 1; i <= tubularSegments; i++) {
// indices
const a = (tubularSegments + 1) * j + i - 1;
const b = (tubularSegments + 1) * (j - 1) + i - 1;
const c = (tubularSegments + 1) * (j - 1) + i;
const d = (tubularSegments + 1) * j + i;
// faces
indicesArray.push(a, b, d);
indicesArray.push(b, c, d);
}
}
return { indicesArray, positionArray, uvArray };
}
// indices
// position
// texcoord
// material
// primitive
// mesh
// node (with mesh and translation 0,0,0)
// scene (with single node)
// I think we make 3 buffers
// (for simplicity, even though they can be combined.)
// indices, positions and uvs
const { indicesArray, positionArray, uvArray } = createTorus();
const indices_uint16 = new Uint16Array(indicesArray);
const position_float32 = new Float32Array(positionArray);
const uv_float32 = new Float32Array(uvArray);
const COMPONENT_TYPE_U16 = 5123;
const COMPONENT_TYPE_F32 = 5126;
const TARGET_ARRAY_BUFFER = 34962; // vertex data
const TARGET_ELEMENT_ARRAY_BUFFER = 34963; // indices
const test_scene = {
asset: {
version: "2.0",
},
buffers: [
{
byteLength: indices_uint16.byteLength,
uri: "data:application/octet-stream;base64,"
+ btoa(String.fromCharCode(...new Uint8Array(indices_uint16.buffer))),
},
{
byteLength: position_float32.byteLength,
uri: "data:application/octet-stream;base64,"
+ btoa(String.fromCharCode(...new Uint8Array(position_float32.buffer))),
},
{
byteLength: uv_float32.byteLength,
uri: "data:application/octet-stream;base64," + btoa(String.fromCharCode(...new Uint8Array(uv_float32.buffer))),
},
],
bufferViews: [

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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"),
},
},
);
if (resp.status == 204) {
console.log("Deleted");
} else {
console.error("Error deleting val:", resp);
return;
}
}
break;
}
case 404: {
console.log("Version already deleted, skipping");
break;
}
default:
console.error("Error fetching val version:", resp);
return;
}
}
}

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
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);
console.log("Done");
}
export async function undo(migrations) {
console.log("Undoing 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) {
console.log("No migrations have been run.");
return;
} else {
step = Number(rs.rows[0][0]);
}
if (step > 0) {
let batch = [];
let i: number = step;
for (i = step - 1; i >= 0; i--) {
let migration = migrations.migrations[i];
let migration_step = i + 1;
batch.push(migration.down);
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);
}
console.log("Done");
}

Display Markdown

Takes a markdown document (as a string) and returns an http handler that renders it.

Example https://www.val.town/v/saolsen/display_markdown_example

Readme
1
2
3
4
5
6
7
8
9
import { gfm } from "https://esm.town/v/saolsen/gfm";
export async function displayMarkdown(markdown: string): Promise<(req: Request) => Response> {
const html = await gfm(markdown);
function handler(req: Request): Response {
return new Response(html, { headers: { "content-type": "text/html" } });
}
return handler;
}

Display Mermaid

Takes a mermaid document (as a string) and returns an http handler that renders it.

Example https://www.val.town/v/saolsen/display_mermaid_example

import { displayMermaid } from "https://esm.town/v/saolsen/display_mermaid";

const mermaid = `
sequenceDiagram
    Alice->>John: Hello John, how are you?
    John-->>Alice: Great!
    Alice-)John: See you later!
`;

export default displayMermaid(mermaid);
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
/// Create a handler function that displays the mermaid diagram.
export function displayMermaid(mermaid: string): (req: Request) => Response {
function handler(req: Request): Response {
return new Response(
`
<html>
<head>
<title>nn</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0">
</meta>
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: true });
</script>
</head>
<body>
<pre class="mermaid">
${mermaid}
</pre>
</body>
</html>`,
{ headers: { "Content-Type": "text/html" } },
);
}
return handler;
}

P5 sketch

Easily turn a p5.js sketch into a val. See https://www.val.town/v/saolsen/p5_sketch for an example.

Usage

  • Make a p5 sketch, you can import the p5 types to make it easier.
import type * as p5 from "npm:@types/p5";
  • Export any "global" p5 functions. These are functions like setup and draw that p5 will call.

  • Set the val type to http and default export the result of sketch, passing in import.meta.url.

A full example looks like this.

import type * as p5 from "npm:@types/p5";

export function setup() {
  createCanvas(400, 400);
}

export function draw() {
  if (mouseIsPressed) {
    fill(0);
  } else {
    fill(255);
  }
  ellipse(mouseX, mouseY, 80, 80);
}

import { sketch } from "https://esm.town/v/saolsen/p5";
export default sketch(import.meta.url);

How it works

The sketch function returns an http handler that sets up a basic page with p5.js added. It then imports your module from the browser and wires up all the exports so p5.js can see them. All the code in your val will run in the browser (except for the default sketch export) so you can't call any Deno functions, environment variables, or other server side apis.

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
export function sketch(module: string): (req: Request) => Response {
return function(req: Request): Response {
return new Response(
`
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"
integrity="sha512-N4kV7GkNv7QR7RX9YF/olywyIgIwNvfEe2nZtfyj73HdjCUkAfOBDbcuJ/cTaN04JKRnw1YG1wnUyNKMsNgg3g=="
crossorigin="anonymous"
referrerpolicy="no-referrer">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/addons/p5.sound.min.js"
integrity="sha512-WzkwpdWEMAY/W8WvP9KS2/VI6zkgejR4/KTxTl4qHx0utqeyVE0JY+S1DlMuxDChC7x0oXtk/ESji6a0lP/Tdg=="
crossorigin="anonymous"
referrerpolicy="no-referrer">
</script>
<link rel="shortcut icon" href="https://p5js.org/assets/img/favicon.ico" />
<link rel="icon" href="https://p5js.org/assets/img/favicon.ico" />
<style>
* {
padding: 0px;
margin: 0px;
}
</style>
<script type="module">
import * as sketch from "${module}";
for (let f of Object.getOwnPropertyNames(sketch)) {
if (f !== "default") {
window[f] = sketch[f];
}
}
</script>
</head>
<body>
</body>
</html>`,
{ headers: { "Content-Type": "text/html" } },
);
};
}
export default async function(req: Request): Promise<Response> {
return new Response(
`
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"
integrity="sha512-N4kV7GkNv7QR7RX9YF/olywyIgIwNvfEe2nZtfyj73HdjCUkAfOBDbcuJ/cTaN04JKRnw1YG1wnUyNKMsNgg3g=="
crossorigin="anonymous"
referrerpolicy="no-referrer">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/addons/p5.sound.min.js"
integrity="sha512-WzkwpdWEMAY/W8WvP9KS2/VI6zkgejR4/KTxTl4qHx0utqeyVE0JY+S1DlMuxDChC7x0oXtk/ESji6a0lP/Tdg=="
crossorigin="anonymous"
referrerpolicy="no-referrer">
</script>
<link rel="shortcut icon" href="https://p5js.org/assets/img/favicon.ico" />
<link rel="icon" href="https://p5js.org/assets/img/favicon.ico" />
<style>
* {
padding: 0px;
margin: 0px;
}
</style>
<script type="module">
import * as sketch from "https://esm.town/v/saolsen/p5_sketch";
for (let f of Object.getOwnPropertyNames(sketch)) {
if (f !== "default") {
window[f] = sketch[f];
}
}
</script>
</head>
<body>
</body>
</html>`,
{ headers: { "Content-Type": "text/html" } },
);
}