goja

package
v1.801.390 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 3, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

Documentation

Overview

Package goja is the in-process JavaScript host: it runs a service's goja bundle (a self-contained, ESM-free JS file exposing globalThis.handle(req)) inside the cloud binary, with an optional tenant-bound Base/SQLite binding.

It is a LIBRARY, not a subsystem — it registers no route and has no manifest row.

It is the SHARED glue used by apps/plan and apps/pricing to host @hanzo/plans and @hanzo/pricing in-process via dop251/goja — the same engine base/plugins/gojavm uses. We do not import base's gojavm Runtime directly because that loader is manifest-driven (extension.json + a single exported `fn` over JSON-over-the-wire payloads); our services instead inject a catalog of JSON globals at VM init and call a richer handle({route,params,...}) entry. The VM-pool + compile-once + per-runtime ensureLoaded discipline here mirrors gojavm/runtime.go exactly so behavior and the pool semantics are identical.

Module boundary: the JS bundle + catalog data live in the service repos (hanzoai/plans, hanzoai/pricing) and are passed in by the caller. This package carries zero service logic — only the engine plumbing.

READ-WRITE variant: this package hosts bundles with a read-only catalog injected once at New (plans/pricing). Subsystems that need PERSISTENCE — a bundle that reads AND writes per-tenant Base/SQLite (captable #97, esign #100, dataroom #101) — use NewBase (base.go / basestore.go), the Base binding folded into THIS package: it builds on this engine (via DispatchWith) and injects a tenant-bound __db bridge per request. Reach for NewBase when your bundle stores data; reach for New directly only for a read-only bundle.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func TenantSegment

func TenantSegment(tenant string) string

TenantSegment maps a tenant/org key to an INJECTIVE, traversal-safe single OBJECT-STORE key segment: lowercased unpadded base32 of the RAW org bytes.

This is deliberately NOT a "slug": lowercasing + folding illegal bytes to '_' (the previous slugify) is NON-injective and collapses DISTINCT owners onto ONE physical prefix — a cross-tenant break. principal.Org returns the org VERBATIM for exactly that reason (folding "Acme"/"acme" or "a b"/"a_b" into one bucket is itself a tenant break); this encoder preserves that distinction all the way to the stored name. base32 of the raw bytes is a bijection with its output, so distinct orgs ALWAYS map to distinct segments: "Acme", "acme", "a b" and "a_b" each land on their OWN prefix (the previous slugify collapsed all four onto two).

The [a-z2-7] output contains no path separator and can never equal "." or ".." (those need chars outside the alphabet), so a segment can never traverse an object-store key prefix. Empty tenant → "" (callers reject it).

It does NOT name the per-tenant DATABASE. That name is namespace's — one slugger for every file cloud opens — and this encoder is what remains for the blob half, which has no namespace rendering of its own.

Types

type BaseConfig

type BaseConfig struct {
	// Name identifies the subsystem ("captable", "esign", "dataroom"). It names
	// the goja host AND the per-tenant database it opens.
	Name string
	// Bundle is the self-contained goja bundle exposing globalThis.handle.
	Bundle []byte
	// Schema is the per-tenant SQLite DDL, run (idempotently — use
	// CREATE TABLE IF NOT EXISTS) on every tenant DB when it first opens.
	Schema string
	// DataDir is the deployment data root; per-tenant files land where namespace
	// renders them, at {DataDir}/orgs/{slug}/{Name}.db.
	DataDir string
	// OnOpen is an optional per-tenant seed hook run ONCE after migration (e.g.
	// captable seeds the tenant's company row). It runs outside the per-request
	// transaction. May be nil.
	OnOpen func(ctx context.Context, tenant string, db *sql.DB) error
	// HostFns are OPTIONAL extra native host globals injected onto the runtime on
	// every Dispatch, ALONGSIDE __db/__newId/__now — for capabilities goja cannot
	// provide that a subsystem implements in Go (e.g. esign injects __pdf =
	// { stamp, sign } for PDF rendering + x509/PKCS#7 signing). Values are Go
	// funcs or map[string]any of Go funcs (goja exposes them as callable JS). They
	// are process-global (set once at NewBase), not per-tenant; the binding stays
	// domain-free. May be nil. A key MUST NOT collide with __db/__newId/__now/__blob.
	HostFns map[string]any
	// Blob is the OPTIONAL object-storage seam (see BlobStore). When set, the binding
	// injects globalThis.__blob = { put(key, b64), get(key) -> b64 } on every
	// Dispatch, bound to the tenant: keys are prefixed with {Name}/{TenantSegment}
	// so a bundle can NEVER address another tenant's blob. Payloads cross as base64
	// strings (goja-friendly); the binding decodes/encodes at the boundary so the
	// bundle never handles raw bytes. nil ⇒ no __blob is injected. This is the ONE
	// way a bundle keeps big binaries out of its per-tenant SQLite.
	Blob BlobStore
}

BaseConfig configures a BaseHost.

type BaseHost

type BaseHost struct {
	// contains filtered or unexported fields
}

BaseHost is a compiled bundle + its per-tenant Base stores. Safe for concurrent use.

func NewBase

func NewBase(cfg BaseConfig) (*BaseHost, error)

NewBase compiles the bundle (via the goja engine, New) and prepares the per-tenant store manager. It does NOT open any tenant DB — those open lazily on first Dispatch.

func (*BaseHost) Close

func (h *BaseHost) Close() error

Close closes every open tenant DB and drops the goja engine. Idempotent.

func (*BaseHost) Dispatch

func (h *BaseHost) Dispatch(ctx context.Context, tenant string, req BaseRequest) (*Response, error)

Dispatch resolves the tenant's Base/SQLite store, opens a per-request transaction, injects the RW-Base host globals bound to it, and calls globalThis.handle. It commits on a <400 non-throwing response and rolls back otherwise. tenant MUST be a validated principal's org (the caller resolves it, e.g. via clients/principal.Org) — the binding does not itself authenticate.

type BaseRequest

type BaseRequest struct {
	Route  string
	Params map[string]string
	Query  map[string]string
	Body   any
}

BaseRequest is the dispatch envelope. The binding adds the tenant (as orgId) and the Base bridge; the caller supplies route/params/query/body.

type BlobStore

type BlobStore interface {
	Put(ctx context.Context, key string, payload []byte) error
	Get(ctx context.Context, key string) ([]byte, error)
}

BlobStore is the ONE object-storage seam a bundle uses to persist large binary payloads OUTSIDE its per-tenant SQLite (e.g. sign's PDFs — a 32 MiB base64 blob in a TEXT column would bloat the tenant DB and get copied on every read). The cloud VFS/S3 data plane (deps.VFS) satisfies it, exactly as clients/dataroom already uses it for document bytes. Keys are opaque; the binding tenant-scopes them.

type Config

type Config struct {
	// Name identifies the service for error messages ("plans", "pricing").
	Name string
	// Bundle is the goja bundle source (goja/bundle.js from the service repo).
	Bundle []byte
	// Globals are injected onto each runtime before the bundle runs, e.g.
	// {"__PLANS_DATA__": <catalog>}. Values are converted via goja.ToValue.
	// Pointers to the same Go value are shared read-only across runtimes; the
	// bundles never mutate injected globals.
	Globals map[string]any
}

Config configures a Host.

type Host

type Host struct {
	// contains filtered or unexported fields
}

Host is a compiled service bundle plus a pool of goja runtimes that have had the bundle + the injected globals evaluated. Safe for concurrent use.

func New

func New(cfg Config) (*Host, error)

New compiles the bundle and pre-warms the runtime pool. The bundle is compiled once (goja.Program is safe to share across runtimes); each pool runtime evaluates it lazily on first use.

func (*Host) Close

func (h *Host) Close() error

Close drops the pool.

func (*Host) Dispatch

func (h *Host) Dispatch(ctx context.Context, req Request) (*Response, error)

Dispatch calls globalThis.handle(req) on a pooled runtime and returns the JS-side {status, body}. ctx cancellation interrupts the call. Read-only bundles (plans/pricing) use this; their globals are the catalog injected once at New.

func (*Host) DispatchWith

func (h *Host) DispatchWith(ctx context.Context, req Request, hostGlobals map[string]any) (*Response, error)

DispatchWith is Dispatch plus a set of per-call NATIVE globals installed on the runtime immediately before handle() runs (left in place until the next dispatch on that slot overwrites them). This is the read-WRITE extension of the read-only plan/pricing pattern: the NewBase binding passes a tenant-bound __db bridge (+ __newId/__now) here so a bundle's SQL calls hit the right per-tenant Base. The slot is held exclusively for the whole call (withSlot serializes it), so installing globals on the shared runtime is race-free, and values are plain Go funcs/maps that goja converts to callable JS.

func (*Host) Eval

func (h *Host) Eval(ctx context.Context, fnName string, jsonArg []byte) ([]byte, error)

Eval runs an arbitrary JS expression against a pooled runtime (bundle already loaded) and returns the exported Go value. Used by callers that want to invoke a non-route helper the bundle exposes (e.g. applyMarkup).

func (*Host) SetGlobal

func (h *Host) SetGlobal(key string, value any)

SetGlobal updates an injected global and forces every pooled runtime to re-evaluate the bundle on next use (so the new value takes effect). Used by the pricing sync path to swap in freshly-synced data.

type Request

type Request struct {
	Route  string            `json:"route"`
	Params map[string]string `json:"params,omitempty"`
	Query  map[string]string `json:"query,omitempty"`
	Tenant string            `json:"tenant,omitempty"`

	// OrgID is the validated tenant for read-WRITE subsystems (captable). It is
	// passed to handle as req.orgId; the bundle uses it to scope every row.
	// Read-only bundles (plans/pricing) ignore it and read Tenant instead.
	OrgID string `json:"orgId,omitempty"`
	// Body is the decoded request body for mutations, passed to handle as
	// req.body. nil for reads. Whatever json.Unmarshal produced (map/slice/scalar)
	// is converted to a JS value by goja.
	Body any `json:"body,omitempty"`
}

Request is the dispatch envelope handed to globalThis.handle in JS.

type Response

type Response struct {
	Status int             `json:"status"`
	Body   json.RawMessage `json:"body"`
}

Response is what globalThis.handle returns: an HTTP status + an opaque body that the host serializes straight to JSON.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL