mirror of
https://github.com/hanzoai/zip.git
synced 2026-10-30 20:11:40 +00:00
Establish zip as the canonical Hanzo Go web framework with embedded
in-process JS execution and a native ZAP RPC HTTP transport. One Go
binary mounts native handlers, legacy TS/JS handlers, WebSocket, and
the ZAP RPC plane — no separate runtime service, no inter-service RPC,
no container-per-service.
zip/runtime/ (new):
- jsvm.go NewJSRuntime / Eval / RegisterHostFn / LoadModule, backed
by a pool of *goja.Runtime (pure Go, no CGO). Pool pattern
lifted from hanzoai/base/plugins/gojavm, slimmed to run +
forEach so late host-fn / module registration reaches warm
VMs.
- pool.go fixed-size goja VM pool; saturated calls fall back to a
fully-provisioned one-off VM.
- require.go CommonJS-style require(): per-VM module sources + memoized
exports in a Go-side side-table (never visible to JS).
- esbuild.go TranspileToES5: TS/modern-JS -> goja-ready JS via esbuild's
pure-Go API (ES2015 target, CommonJS format).
- handler.go JSHandler / JSModule: mount an Express-shaped (req, res) JS
function as a fiber.Handler; req mirrors method/path/query/
headers/body, res mirrors status/set/json/send.
- json.go encode/decode through internal/jsonenc — one wire format.
zip/zaprpc/ (new on existing Registry seam):
- envelope.go canonical ZAP RPC frame (service/method/payload) over the
zap-proto/go wire codec.
- http.go HTTPHandler(reg): serves the ZAP RPC plane over HTTP POST;
binary envelope canonical, X-ZAP-* headers are a hint only.
examples/express-in-zip/ (new): legacy app.ts -> real esbuild -> real
goja -> real Fiber -> real HTTP, with an integration test proving
GET /legacy/foo and POST /legacy/bar body echo.
README: embedded-JS migration path + single-binary (HIP-0106) sections.
go build ./... and go test ./... (incl. -race) pass.
74 lines
1.7 KiB
Go
74 lines
1.7 KiB
Go
package runtime
|
|
|
|
import (
|
|
"sync"
|
|
|
|
"github.com/dop251/goja"
|
|
)
|
|
|
|
// poolItem is one pre-warmed VM with a busy flag.
|
|
type poolItem struct {
|
|
mu sync.Mutex
|
|
busy bool
|
|
vm *goja.Runtime
|
|
}
|
|
|
|
// vmPool is a fixed-size pool of goja VMs. Lifted from
|
|
// hanzoai/base/plugins/gojavm and slimmed: zip needs run (borrow one)
|
|
// and forEach (apply a mutation to every pooled VM under its own lock so
|
|
// late RegisterHostFn / LoadModule reach already-warm VMs).
|
|
type vmPool struct {
|
|
factory func() *goja.Runtime
|
|
items []*poolItem
|
|
}
|
|
|
|
func newVMPool(size int, factory func() *goja.Runtime) *vmPool {
|
|
p := &vmPool{factory: factory}
|
|
if size > 0 {
|
|
p.items = make([]*poolItem, size)
|
|
for i := 0; i < size; i++ {
|
|
p.items[i] = &poolItem{vm: factory()}
|
|
}
|
|
}
|
|
return p
|
|
}
|
|
|
|
// run executes call with a pooled VM. If every slot is busy it builds a
|
|
// one-off VM (fully provisioned via factory) and discards it after the
|
|
// call. goja VMs are single-threaded, so a borrowed VM is never touched
|
|
// concurrently.
|
|
func (p *vmPool) run(call func(vm *goja.Runtime) error) error {
|
|
for _, item := range p.items {
|
|
item.mu.Lock()
|
|
if item.busy {
|
|
item.mu.Unlock()
|
|
continue
|
|
}
|
|
item.busy = true
|
|
item.mu.Unlock()
|
|
|
|
err := call(item.vm)
|
|
|
|
item.mu.Lock()
|
|
item.busy = false
|
|
item.mu.Unlock()
|
|
return err
|
|
}
|
|
// Pool saturated: one-off VM, factory provisions host fns + modules.
|
|
return call(p.factory())
|
|
}
|
|
|
|
// forEach applies mutate to every pooled VM, each under its own lock so
|
|
// it cannot race a concurrent run() on the same VM.
|
|
func (p *vmPool) forEach(mutate func(vm *goja.Runtime) error) error {
|
|
for _, item := range p.items {
|
|
item.mu.Lock()
|
|
err := mutate(item.vm)
|
|
item.mu.Unlock()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|