Files
yawn/server/database.go
T
Ampandheaust 7070799862 Add Go CDN and playground server
Build Core and Handles into in-memory CDN modules, add the marketing site and tutorial docs, and provide a SQLite-backed WebGPU playground with TypeScript tooling and profiling.

Amp-Thread-ID: https://ampcode.com/threads/T-01a02485-5574-707c-bff4-5668d83bee8a
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
2026-08-21 16:42:02 +00:00

154 lines
3.8 KiB
Go

package main
import (
"context"
"crypto/rand"
"database/sql"
"errors"
"fmt"
"math/big"
"os"
"path/filepath"
"regexp"
"time"
"unicode/utf8"
_ "github.com/mattn/go-sqlite3"
)
const (
maximumCodeBytes = 200 * 1024
maximumTitleRunes = 80
)
var playgroundIDPattern = regexp.MustCompile(`^[a-z0-9]{6,24}$`)
type playground struct {
ID string `json:"id"`
Revision int `json:"revision"`
Title string `json:"title"`
Code string `json:"code"`
CreatedAt time.Time `json:"createdAt"`
}
type playgroundStore struct {
*sql.DB
}
func openPlaygroundStore(path string) (*playgroundStore, error) {
if path != ":memory:" {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return nil, err
}
}
database, err := sql.Open("sqlite3", path+"?_foreign_keys=on&_busy_timeout=5000&_journal_mode=WAL")
if err != nil {
return nil, err
}
database.SetMaxOpenConns(1)
store := &playgroundStore{DB: database}
if err := store.initialize(); err != nil {
database.Close()
return nil, err
}
return store, nil
}
func (store *playgroundStore) initialize() error {
_, err := store.Exec(`
CREATE TABLE IF NOT EXISTS playgrounds (
id TEXT NOT NULL,
revision INTEGER NOT NULL CHECK (revision > 0),
title TEXT NOT NULL,
code TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (id, revision)
);
CREATE INDEX IF NOT EXISTS playgrounds_created_at
ON playgrounds (created_at DESC);
`)
return err
}
func (store *playgroundStore) save(ctx context.Context, id, title, code string) (playground, error) {
if id != "" && !playgroundIDPattern.MatchString(id) {
return playground{}, errors.New("invalid playground id")
}
if title == "" || utf8.RuneCountInString(title) > maximumTitleRunes {
return playground{}, fmt.Errorf("title must contain 1 to %d characters", maximumTitleRunes)
}
if code == "" || len(code) > maximumCodeBytes || !utf8.ValidString(code) {
return playground{}, fmt.Errorf("code must contain 1 to %d bytes of UTF-8", maximumCodeBytes)
}
if id == "" {
var err error
id, err = randomPlaygroundID()
if err != nil {
return playground{}, err
}
}
transaction, err := store.BeginTx(ctx, nil)
if err != nil {
return playground{}, err
}
defer transaction.Rollback()
var revision int
if err := transaction.QueryRowContext(
ctx,
`SELECT COALESCE(MAX(revision), 0) + 1 FROM playgrounds WHERE id = ?`,
id,
).Scan(&revision); err != nil {
return playground{}, err
}
createdAt := time.Now().UTC().Truncate(time.Millisecond)
if _, err := transaction.ExecContext(
ctx,
`INSERT INTO playgrounds (id, revision, title, code, created_at) VALUES (?, ?, ?, ?, ?)`,
id,
revision,
title,
code,
createdAt.Format(time.RFC3339Nano),
); err != nil {
return playground{}, err
}
if err := transaction.Commit(); err != nil {
return playground{}, err
}
return playground{ID: id, Revision: revision, Title: title, Code: code, CreatedAt: createdAt}, nil
}
func (store *playgroundStore) get(ctx context.Context, id string, revision int) (playground, error) {
if !playgroundIDPattern.MatchString(id) || revision < 1 {
return playground{}, sql.ErrNoRows
}
var result playground
var createdAt string
err := store.QueryRowContext(
ctx,
`SELECT id, revision, title, code, created_at FROM playgrounds WHERE id = ? AND revision = ?`,
id,
revision,
).Scan(&result.ID, &result.Revision, &result.Title, &result.Code, &createdAt)
if err != nil {
return playground{}, err
}
result.CreatedAt, err = time.Parse(time.RFC3339Nano, createdAt)
return result, err
}
func randomPlaygroundID() (string, error) {
const alphabet = "abcdefghijkmnopqrstuvwxyz23456789"
result := make([]byte, 10)
for index := range result {
value, err := rand.Int(rand.Reader, big.NewInt(int64(len(alphabet))))
if err != nil {
return "", err
}
result[index] = alphabet[value.Int64()]
}
return string(result), nil
}