Skip to content

Commit 5ebe510

Browse files
devallibusclaude
andcommitted
feat(playground): add design spec, session store, types, and API routes
Implements Phase B server infrastructure for the Shader Playground (#52): - Design spec with architecture, API contracts, and wireframes - PlaygroundSession types, SSE events, and API request/response types - SQLite session store (playground-db) following reviews-db pattern - In-memory SSE connection registry with screenshot wait queue - Catch-all API route handling create, update, state, screenshot, errors, SSE - 15 unit tests for session store CRUD operations (all passing) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7b16f12 commit 5ebe510

5 files changed

Lines changed: 962 additions & 0 deletions

File tree

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// ---------------------------------------------------------------------------
2+
// Playground shared types — used by server (DB, API) and client (UI, SSE)
3+
// ---------------------------------------------------------------------------
4+
5+
export type UniformDefinition = {
6+
name: string
7+
type: string
8+
defaultValue: unknown
9+
description?: string
10+
min?: number
11+
max?: number
12+
}
13+
14+
export type SessionMetadata = {
15+
name?: string
16+
displayName?: string
17+
summary?: string
18+
tags?: string[]
19+
}
20+
21+
export type PlaygroundSession = {
22+
id: string
23+
vertexSource: string
24+
fragmentSource: string
25+
uniforms: UniformDefinition[]
26+
uniformValues: Record<string, unknown> | null
27+
pipeline: string
28+
compilationErrors: string[]
29+
screenshotBase64: string | null
30+
screenshotAt: string | null
31+
metadata: SessionMetadata | null
32+
createdAt: string
33+
updatedAt: string
34+
}
35+
36+
// ---------------------------------------------------------------------------
37+
// SSE event types
38+
// ---------------------------------------------------------------------------
39+
40+
export type ShaderUpdateEvent = {
41+
type: 'shader_update'
42+
vertexSource: string
43+
fragmentSource: string
44+
}
45+
46+
export type UniformUpdateEvent = {
47+
type: 'uniform_update'
48+
values: Record<string, unknown>
49+
}
50+
51+
export type PlaygroundSSEEvent = ShaderUpdateEvent | UniformUpdateEvent
52+
53+
// ---------------------------------------------------------------------------
54+
// API request / response types
55+
// ---------------------------------------------------------------------------
56+
57+
export type CreateSessionRequest = {
58+
vertexSource?: string
59+
fragmentSource?: string
60+
uniforms?: UniformDefinition[]
61+
pipeline?: string
62+
}
63+
64+
export type CreateSessionResponse = {
65+
sessionId: string
66+
url: string
67+
}
68+
69+
export type UpdateShaderRequest = {
70+
vertexSource?: string
71+
fragmentSource?: string
72+
}
73+
74+
export type UpdateShaderResponse = {
75+
status: 'ok'
76+
compilationErrors: string[]
77+
screenshotBase64: string | null
78+
browserConnected: boolean
79+
}
80+
81+
export type ScreenshotRequest = {
82+
base64: string
83+
}
84+
85+
export type ErrorsResponse = {
86+
errors: string[]
87+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import assert from 'node:assert/strict'
2+
import { mkdtempSync } from 'node:fs'
3+
import { join } from 'node:path'
4+
import { tmpdir } from 'node:os'
5+
6+
// Isolate test DB in a temp directory so runs are idempotent
7+
process.env.DATA_DIR = mkdtempSync(join(tmpdir(), 'playground-test-'))
8+
9+
const {
10+
createSession,
11+
getSession,
12+
updateShader,
13+
setScreenshot,
14+
setErrors,
15+
setUniformValues,
16+
updateMetadata,
17+
} = await import('./playground-db.ts')
18+
19+
function runTest(name: string, callback: () => void | Promise<void>) {
20+
const result = callback()
21+
if (result instanceof Promise) {
22+
result.then(
23+
() => console.log(`ok ${name}`),
24+
(error) => {
25+
console.error(`not ok ${name}`)
26+
throw error
27+
},
28+
)
29+
return result
30+
}
31+
console.log(`ok ${name}`)
32+
}
33+
34+
// ---------------------------------------------------------------------------
35+
// Tests
36+
// ---------------------------------------------------------------------------
37+
38+
runTest('createSession returns id and session with defaults', () => {
39+
const { id, session } = createSession()
40+
assert.equal(typeof id, 'string')
41+
assert.ok(id.length > 0)
42+
assert.equal(session.id, id)
43+
assert.ok(session.vertexSource.includes('gl_Position'))
44+
assert.ok(session.fragmentSource.includes('gl_FragColor'))
45+
assert.equal(session.pipeline, 'surface')
46+
assert.equal(session.uniforms.length, 1)
47+
assert.equal(session.uniforms[0]!.name, 'uTime')
48+
assert.deepEqual(session.compilationErrors, [])
49+
assert.equal(session.screenshotBase64, null)
50+
assert.equal(session.metadata, null)
51+
})
52+
53+
runTest('createSession accepts custom GLSL', () => {
54+
const { session } = createSession({
55+
vertexSource: 'void main() { gl_Position = vec4(0.0); }',
56+
fragmentSource: 'void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); }',
57+
pipeline: 'postprocessing',
58+
uniforms: [{ name: 'uColor', type: 'vec3', defaultValue: [1, 0, 0] }],
59+
})
60+
assert.ok(session.vertexSource.includes('vec4(0.0)'))
61+
assert.ok(session.fragmentSource.includes('1.0, 0.0, 0.0'))
62+
assert.equal(session.pipeline, 'postprocessing')
63+
assert.equal(session.uniforms.length, 1)
64+
assert.equal(session.uniforms[0]!.name, 'uColor')
65+
})
66+
67+
runTest('getSession returns null for unknown id', () => {
68+
const session = getSession('nonexistent-id')
69+
assert.equal(session, null)
70+
})
71+
72+
runTest('getSession returns created session', () => {
73+
const { id } = createSession()
74+
const session = getSession(id)
75+
assert.ok(session)
76+
assert.equal(session.id, id)
77+
})
78+
79+
runTest('updateShader updates vertex source', () => {
80+
const { id } = createSession()
81+
const newVertex = 'void main() { gl_Position = vec4(1.0); }'
82+
updateShader(id, { vertexSource: newVertex })
83+
const session = getSession(id)!
84+
assert.equal(session.vertexSource, newVertex)
85+
// Fragment should remain the default
86+
assert.ok(session.fragmentSource.includes('gl_FragColor'))
87+
})
88+
89+
runTest('updateShader updates fragment source', () => {
90+
const { id } = createSession()
91+
const newFrag = 'void main() { gl_FragColor = vec4(0.0); }'
92+
updateShader(id, { fragmentSource: newFrag })
93+
const session = getSession(id)!
94+
assert.equal(session.fragmentSource, newFrag)
95+
})
96+
97+
runTest('updateShader updates both sources', () => {
98+
const { id } = createSession()
99+
const newVertex = 'vertex shader'
100+
const newFrag = 'fragment shader'
101+
updateShader(id, { vertexSource: newVertex, fragmentSource: newFrag })
102+
const session = getSession(id)!
103+
assert.equal(session.vertexSource, newVertex)
104+
assert.equal(session.fragmentSource, newFrag)
105+
})
106+
107+
runTest('updateShader with empty object is a no-op', () => {
108+
const { id, session: original } = createSession()
109+
updateShader(id, {})
110+
const session = getSession(id)!
111+
assert.equal(session.vertexSource, original.vertexSource)
112+
assert.equal(session.fragmentSource, original.fragmentSource)
113+
})
114+
115+
runTest('setScreenshot stores base64 data', () => {
116+
const { id } = createSession()
117+
setScreenshot(id, 'data:image/png;base64,abc123')
118+
const session = getSession(id)!
119+
assert.equal(session.screenshotBase64, 'data:image/png;base64,abc123')
120+
assert.ok(session.screenshotAt)
121+
})
122+
123+
runTest('setErrors stores compilation errors', () => {
124+
const { id } = createSession()
125+
const errors = ["ERROR: 0:5: 'foo' : undeclared identifier", 'ERROR: 0:10: syntax error']
126+
setErrors(id, errors)
127+
const session = getSession(id)!
128+
assert.deepEqual(session.compilationErrors, errors)
129+
})
130+
131+
runTest('setErrors with empty array clears errors', () => {
132+
const { id } = createSession()
133+
setErrors(id, ['some error'])
134+
setErrors(id, [])
135+
const session = getSession(id)!
136+
assert.deepEqual(session.compilationErrors, [])
137+
})
138+
139+
runTest('setUniformValues stores values', () => {
140+
const { id } = createSession()
141+
const values = { uTime: 1.5, uColor: [1, 0, 0] }
142+
setUniformValues(id, values)
143+
const session = getSession(id)!
144+
assert.deepEqual(session.uniformValues, values)
145+
})
146+
147+
runTest('updateMetadata stores metadata', () => {
148+
const { id } = createSession()
149+
const metadata = { name: 'test-shader', displayName: 'Test Shader', summary: 'A test', tags: ['test'] }
150+
updateMetadata(id, metadata)
151+
const session = getSession(id)!
152+
assert.deepEqual(session.metadata, metadata)
153+
})
154+
155+
runTest('updateMetadata overwrites previous metadata', () => {
156+
const { id } = createSession()
157+
updateMetadata(id, { name: 'old' })
158+
updateMetadata(id, { name: 'new', tags: ['updated'] })
159+
const session = getSession(id)!
160+
assert.equal(session.metadata!.name, 'new')
161+
assert.deepEqual(session.metadata!.tags, ['updated'])
162+
})
163+
164+
runTest('multiple sessions are independent', () => {
165+
const { id: id1 } = createSession({ fragmentSource: 'shader1' })
166+
const { id: id2 } = createSession({ fragmentSource: 'shader2' })
167+
assert.notEqual(id1, id2)
168+
assert.equal(getSession(id1)!.fragmentSource, 'shader1')
169+
assert.equal(getSession(id2)!.fragmentSource, 'shader2')
170+
})
171+
172+
console.log('playground-db tests passed')

0 commit comments

Comments
 (0)