Skip to content

Commit 7e1d42b

Browse files
committed
workerd stuff
1 parent 4992579 commit 7e1d42b

File tree

23 files changed

+3764
-3249
lines changed

23 files changed

+3764
-3249
lines changed

apps/dws/api/workers/runtime.ts

Lines changed: 246 additions & 79 deletions
Large diffs are not rendered by default.

apps/dws/test-worker-runtime.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/**
2+
* Test script for worker runtime
3+
* Run: cd apps/dws && bun run test-worker-runtime.ts
4+
*/
5+
6+
import { mkdir, rm } from 'node:fs/promises'
7+
import { createBackendManager } from './api/storage/backends'
8+
import { WorkerRuntime } from './api/workers/runtime'
9+
import type { WorkerFunction } from './api/workers/types'
10+
11+
// Create a simple test worker
12+
const TEST_WORKER_CODE = `
13+
// Simple test worker
14+
const PORT = process.env.PORT || 3000;
15+
16+
const server = Bun.serve({
17+
port: Number(PORT),
18+
fetch(req) {
19+
const url = new URL(req.url);
20+
21+
if (url.pathname === '/health') {
22+
return new Response('OK', { status: 200 });
23+
}
24+
25+
if (url.pathname === '/echo') {
26+
return new Response(JSON.stringify({
27+
method: req.method,
28+
path: url.pathname,
29+
timestamp: Date.now(),
30+
}), {
31+
headers: { 'Content-Type': 'application/json' }
32+
});
33+
}
34+
35+
return new Response('Hello from test worker!', { status: 200 });
36+
},
37+
});
38+
39+
console.log('Test worker listening on port ' + PORT);
40+
`;
41+
42+
async function main() {
43+
console.log('=== Worker Runtime Test ===\n')
44+
45+
// Create a mock backend that stores code in memory
46+
const codeStore = new Map<string, Buffer>()
47+
48+
// Upload test worker code
49+
const testCid = 'test-worker-' + Date.now()
50+
codeStore.set(testCid, Buffer.from(TEST_WORKER_CODE))
51+
52+
// Create mock backend manager
53+
const mockBackend = {
54+
upload: async (content: Buffer, _opts: unknown) => ({ cid: testCid }),
55+
download: async (cid: string) => {
56+
const content = codeStore.get(cid)
57+
if (!content) throw new Error('Not found: ' + cid)
58+
return { content }
59+
},
60+
exists: async (cid: string) => codeStore.has(cid),
61+
delete: async (_cid: string) => {},
62+
list: async () => [],
63+
getUrl: (cid: string) => `local://${cid}`,
64+
stats: async () => ({ totalSize: 0, fileCount: 0 }),
65+
}
66+
67+
// Create runtime
68+
console.log('1. Creating worker runtime...')
69+
const runtime = new WorkerRuntime(mockBackend as unknown as ReturnType<typeof createBackendManager>)
70+
71+
// Wait for initialization
72+
await new Promise(r => setTimeout(r, 1000))
73+
74+
// Deploy a test function
75+
console.log('\n2. Deploying test worker...')
76+
const fn: WorkerFunction = {
77+
id: 'test-function-1',
78+
name: 'test-worker',
79+
owner: '0x0000000000000000000000000000000000000000',
80+
runtime: 'bun',
81+
handler: 'index.handler',
82+
codeCid: testCid,
83+
memory: 128,
84+
timeout: 30000,
85+
env: {},
86+
status: 'active',
87+
version: 1,
88+
createdAt: Date.now(),
89+
updatedAt: Date.now(),
90+
invocationCount: 0,
91+
avgDurationMs: 0,
92+
errorCount: 0,
93+
}
94+
95+
await runtime.deployFunction(fn)
96+
console.log(' Deployed: ' + fn.name)
97+
98+
// Get stats
99+
const stats = runtime.getStats()
100+
console.log('\n3. Runtime stats:', JSON.stringify(stats, null, 2))
101+
102+
// Test HTTP invocation
103+
console.log('\n4. Testing HTTP invocation...')
104+
const response = await runtime.invokeHTTP(fn.id, {
105+
method: 'GET',
106+
path: '/echo',
107+
headers: {},
108+
query: {},
109+
body: null,
110+
})
111+
112+
console.log(' Response status:', response.statusCode)
113+
console.log(' Response body:', response.body)
114+
115+
if (response.statusCode === 200) {
116+
console.log('\n✅ Worker runtime test PASSED!')
117+
} else if (response.statusCode === 503) {
118+
console.log('\n⚠️ Worker failed to spawn. This is expected if running in a restricted environment.')
119+
console.log(' Error:', response.body)
120+
} else {
121+
console.log('\n❌ Worker runtime test FAILED!')
122+
console.log(' Error:', response.body)
123+
}
124+
125+
// Undeploy
126+
console.log('\n5. Cleaning up...')
127+
await runtime.undeployFunction(fn.id)
128+
129+
// Final stats
130+
const finalStats = runtime.getStats()
131+
console.log(' Final stats:', JSON.stringify(finalStats, null, 2))
132+
133+
console.log('\n=== Test complete ===')
134+
}
135+
136+
main().catch(console.error)

apps/gateway/api/worker.ts

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/**
2+
* Gateway API Worker
3+
*
4+
* DWS-deployable worker using Elysia with workerd compatibility.
5+
* Compatible with workerd runtime and DWS infrastructure.
6+
*
7+
* @see https://elysiajs.com/integrations/cloudflare-worker
8+
*/
9+
10+
import { cors } from '@elysiajs/cors'
11+
import {
12+
CORE_PORTS,
13+
getCoreAppUrl,
14+
getCurrentNetwork,
15+
getLocalhostHost,
16+
} from '@jejunetwork/config'
17+
import { Elysia } from 'elysia'
18+
import { z } from 'zod'
19+
import { config } from './config'
20+
import {
21+
claimFromFaucet,
22+
getFaucetInfo,
23+
getFaucetStatus,
24+
} from './services/faucet-service'
25+
26+
/**
27+
* Worker Environment Types
28+
*/
29+
export interface GatewayEnv {
30+
// Standard workerd bindings
31+
NETWORK: 'localnet' | 'testnet' | 'mainnet'
32+
RPC_URL: string
33+
FAUCET_PRIVATE_KEY?: string
34+
35+
// KV bindings (optional)
36+
GATEWAY_CACHE?: KVNamespace
37+
}
38+
39+
interface KVNamespace {
40+
get(key: string): Promise<string | null>
41+
put(
42+
key: string,
43+
value: string,
44+
options?: { expirationTtl?: number },
45+
): Promise<void>
46+
delete(key: string): Promise<void>
47+
}
48+
49+
const AddressSchema = z.string().regex(/^0x[a-fA-F0-9]{40}$/)
50+
51+
/**
52+
* Create the Gateway Elysia app
53+
*/
54+
export function createGatewayApp(env?: Partial<GatewayEnv>) {
55+
const isDev = env?.NETWORK === 'localnet'
56+
const network = env?.NETWORK ?? getCurrentNetwork()
57+
58+
const app = new Elysia().use(
59+
cors({
60+
origin: isDev
61+
? true
62+
: [
63+
'https://gateway.jejunetwork.org',
64+
'https://jejunetwork.org',
65+
getCoreAppUrl('GATEWAY'),
66+
],
67+
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
68+
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
69+
credentials: true,
70+
}),
71+
)
72+
73+
// Health check
74+
app.get('/health', () => ({
75+
status: 'ok',
76+
service: 'gateway-api',
77+
version: '1.0.0',
78+
network,
79+
runtime: 'workerd',
80+
endpoints: {
81+
faucet: '/api/faucet',
82+
rpc: '/rpc',
83+
x402: '/x402',
84+
oracle: '/oracle',
85+
leaderboard: '/leaderboard',
86+
},
87+
}))
88+
89+
// Faucet routes
90+
app.group('/api/faucet', (app) =>
91+
app
92+
.get('/info', () => getFaucetInfo())
93+
.get('/status/:address', async ({ params }) => {
94+
const parsed = AddressSchema.safeParse(params.address)
95+
if (!parsed.success) {
96+
return { error: 'Invalid address format' }
97+
}
98+
return getFaucetStatus(parsed.data as `0x${string}`)
99+
})
100+
.post('/claim', async ({ body }) => {
101+
const bodyParsed = z.object({ address: AddressSchema }).safeParse(body)
102+
if (!bodyParsed.success) {
103+
return { success: false, error: 'Invalid address format' }
104+
}
105+
return claimFromFaucet(bodyParsed.data.address as `0x${string}`)
106+
}),
107+
)
108+
109+
// Root route - API info
110+
app.get('/', () => ({
111+
name: 'Gateway API',
112+
version: '1.0.0',
113+
description: 'Jeju Network Gateway - Faucet, RPC Proxy, x402 Payments, Oracle',
114+
runtime: 'workerd',
115+
network,
116+
endpoints: {
117+
health: '/health',
118+
faucet: '/api/faucet',
119+
rpc: '/rpc',
120+
x402: '/x402',
121+
oracle: '/oracle',
122+
leaderboard: '/leaderboard',
123+
},
124+
}))
125+
126+
// Agent card endpoint
127+
app.get('/.well-known/agent-card.json', () => ({
128+
name: 'Gateway',
129+
description: 'Jeju Network Gateway - Faucet, RPC Proxy, x402 Payments, Oracle',
130+
version: '1.0.0',
131+
skills: [
132+
{ id: 'faucet-claim', name: 'Claim Faucet', description: 'Claim testnet tokens from faucet' },
133+
{ id: 'faucet-status', name: 'Faucet Status', description: 'Check faucet claim status' },
134+
{ id: 'rpc-proxy', name: 'RPC Proxy', description: 'Proxy JSON-RPC requests' },
135+
{ id: 'x402-verify', name: 'Verify Payment', description: 'Verify x402 payments' },
136+
],
137+
endpoints: {
138+
a2a: '/a2a',
139+
mcp: '/mcp',
140+
},
141+
}))
142+
143+
return app
144+
}
145+
146+
// Worker Export (for DWS/workerd)
147+
148+
/**
149+
* Workerd/Cloudflare Workers execution context
150+
*/
151+
interface ExecutionContext {
152+
waitUntil(promise: Promise<unknown>): void
153+
passThroughOnException(): void
154+
}
155+
156+
/**
157+
* Cached app instance for worker reuse
158+
*/
159+
let cachedApp: ReturnType<typeof createGatewayApp> | null = null
160+
let cachedEnvHash: string | null = null
161+
162+
function getAppForEnv(env: GatewayEnv): ReturnType<typeof createGatewayApp> {
163+
const envHash = `${env.NETWORK}`
164+
165+
if (cachedApp && cachedEnvHash === envHash) {
166+
return cachedApp
167+
}
168+
169+
cachedApp = createGatewayApp(env).compile()
170+
cachedEnvHash = envHash
171+
return cachedApp
172+
}
173+
174+
/**
175+
* Default export for workerd/Cloudflare Workers
176+
*/
177+
export default {
178+
async fetch(
179+
request: Request,
180+
env: GatewayEnv,
181+
_ctx: ExecutionContext,
182+
): Promise<Response> {
183+
const app = getAppForEnv(env)
184+
return app.handle(request)
185+
},
186+
}
187+
188+
// Standalone Server (for local dev)
189+
190+
const isMainModule = typeof Bun !== 'undefined' && import.meta.path === Bun.main
191+
192+
if (isMainModule) {
193+
const PORT = config.gatewayApiPort || CORE_PORTS.NODE_EXPLORER_API.get()
194+
const network = getCurrentNetwork()
195+
196+
const app = createGatewayApp({
197+
NETWORK: network,
198+
RPC_URL: process.env.RPC_URL || 'http://localhost:8545',
199+
FAUCET_PRIVATE_KEY: process.env.FAUCET_PRIVATE_KEY,
200+
})
201+
202+
const host = getLocalhostHost()
203+
app.listen(PORT, () => {
204+
console.log(`[Gateway] Worker running at http://${host}:${PORT}`)
205+
console.log(`[Gateway] Network: ${network}`)
206+
})
207+
}

apps/gateway/scripts/build.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,47 @@ async function build() {
182182
}
183183
console.log('[Gateway] API servers built')
184184

185+
// Build worker for workerd deployment
186+
console.log('[Gateway] Building worker for DWS deployment...')
187+
mkdirSync(join(outdir, 'worker'), { recursive: true })
188+
const workerResult = await Bun.build({
189+
entrypoints: [resolve(APP_DIR, 'api/worker.ts')],
190+
outdir: join(outdir, 'worker'),
191+
target: 'bun',
192+
minify: true,
193+
sourcemap: 'external',
194+
external: [
195+
'bun:sqlite',
196+
'child_process',
197+
'node:child_process',
198+
'node:fs',
199+
'node:path',
200+
'node:crypto',
201+
],
202+
define: { 'process.env.NODE_ENV': JSON.stringify('production') },
203+
})
204+
205+
if (!workerResult.success) {
206+
console.error('[Gateway] Worker build failed:')
207+
for (const log of workerResult.logs) console.error(log)
208+
throw new Error('Worker build failed')
209+
}
210+
211+
// Write worker metadata
212+
const metadata = {
213+
name: 'gateway-api',
214+
version: '1.0.0',
215+
entrypoint: 'worker.js',
216+
compatibilityDate: '2024-01-01',
217+
buildTime: new Date().toISOString(),
218+
runtime: 'workerd',
219+
}
220+
writeFileSync(
221+
join(outdir, 'worker', 'metadata.json'),
222+
JSON.stringify(metadata, null, 2),
223+
)
224+
console.log('[Gateway] Worker built successfully')
225+
185226
// Find the main entry file with hash
186227
const mainEntry = frontendResult.outputs.find(
187228
(o) => o.kind === 'entry-point' && o.path.includes('main'),

0 commit comments

Comments
 (0)