a post-component library for building user-interfaces on the web.
1import { createBirpc } from 'birpc'
2import * as mitata from 'mitata'
3import * as devalue from './devalue.ts'
4import type { ServerFunctions } from './main.ts'
5import { assert } from './test.ts'
6
7export type TestResult = { name: string } & ({ result: 'pass'; duration: number } | { result: 'fail'; reason: unknown })
8
9export interface ClientFunctions {
10 define<K extends keyof typeof globalThis>(name: K, value: (typeof globalThis)[K]): void
11 import(path: string): Promise<unknown>
12 run_tests(options: { filter?: RegExp }): Promise<void>
13 run_benchmarks(options: { filter?: RegExp; builds?: Array<{ dir: string; ref: string }> }): Promise<mitata.trial[]>
14 stop_coverage(): Promise<void>
15}
16
17const client: ClientFunctions = {
18 define(name, value) {
19 globalThis[name] = value
20 },
21 import(path) {
22 return import(path)
23 },
24 async run_tests(options) {
25 for (const test of tests) {
26 if (options.filter?.test(test.name) === false) continue
27
28 try {
29 const start = performance.now()
30 await test.fn()
31 const end = performance.now()
32 await server.report_result({ name: test.name, result: 'pass', duration: end - start })
33 } catch (error) {
34 await server.report_result({ name: test.name, result: 'fail', reason: error })
35 }
36 }
37 },
38 async run_benchmarks(options) {
39 const { builds } = options
40
41 if (!builds) {
42 const { benchmarks } = await mitata.run({ filter: options.filter })
43 return benchmarks
44 } else {
45 const versions = await Promise.all(
46 builds.map(async ({ dir, ref }) => {
47 return {
48 index: (await import(`../../${dir}/dist/index.min.js`)) as typeof import('dhtml'),
49 client: (await import(`../../${dir}/dist/client.min.js`)) as typeof import('dhtml/client'),
50 server: (await import(`../../${dir}/dist/server.min.js`)) as typeof import('dhtml/server'),
51 ref,
52 }
53 }),
54 )
55
56 // TODO: much like tests, export a custom bench() function from test.ts,
57 // which collects in an array defined in this file, so that we don't need
58 // to hardcode the bench-comparison path.
59 // unlike test(), bench() will provide the lib argument to the function.
60 const { get_benchmarks } = await import('../../../src/client/tests/bench-comparison.ts' as string)
61
62 const called_versions = versions.map(lib => ({
63 ref: lib.ref,
64 fns: Object.entries(get_benchmarks(lib))
65 .filter(([name]) => (options.filter === undefined ? true : options.filter.test(name)))
66 .map(([, value]) => {
67 assert(typeof value === 'function')
68 return value
69 }),
70 }))
71
72 const n_fns = called_versions[0].fns.length
73 assert(n_fns > 0, 'filtered to 0 functions')
74 for (const { fns } of called_versions.slice(1)) {
75 assert(fns.length === n_fns, 'expected the same number of functions from all modules')
76 }
77
78 // Warmup runs for each version to reduce cache effects
79 console.log('Running warmup iterations...')
80 for (let warmup = 0; warmup < 3; warmup++) {
81 for (const { ref, fns } of called_versions) {
82 for (const fn of fns) fn()
83 }
84 }
85 console.log('Warmup complete, starting benchmarks...')
86
87 mitata.summary(() => {
88 for (const { ref, fns } of called_versions) {
89 mitata
90 .bench(ref, () => {
91 for (const fn of fns) fn()
92 })
93 .gc('inner')
94 }
95 })
96
97 const { benchmarks } = await mitata.run()
98 return benchmarks
99 }
100 },
101 async stop_coverage() {
102 if (typeof process === 'undefined') return
103 const v8 = await import('node:v8')
104 v8.takeCoverage()
105 v8.stopCoverage()
106 },
107}
108
109declare global {
110 var __onmessage: (fn: (data: any) => void) => void
111 var __postMessage: (data: any) => void
112}
113
114const server = createBirpc<ServerFunctions, ClientFunctions>(
115 client,
116 typeof process === 'undefined'
117 ? {
118 post: window.__postMessage,
119 on: fn => (window.__onmessage = fn),
120 serialize: devalue.stringify,
121 deserialize: devalue.parse,
122 }
123 : {
124 post: data => process.send!(data),
125 on: fn => process.on('message', fn),
126 serialize: devalue.stringify,
127 deserialize: devalue.parse,
128 },
129)
130
131export const tests: Array<{ name: string; fn: () => void | Promise<void> }> = []