mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-06-16 23:14:13 +00:00
feat(serverless): register host module under "orama" alias
- Add "orama" to the list of host module registration names to support
common developer intuition and prevent instantiation errors.
- Add comprehensive regression tests to ensure all aliases ("env",
"host", "orama") remain registered.
- Update SDK documentation to clarify import conventions and alias
support.
This commit is contained in:
parent
7738eee041
commit
bd26af2cb1
@ -397,9 +397,23 @@ func (e *Engine) logInvocation(ctx context.Context, fn *Function, invCtx *Invoca
|
||||
}
|
||||
|
||||
// registerHostModule registers the Orama host functions with the wazero runtime.
|
||||
//
|
||||
// We expose the SAME export set under three module names:
|
||||
//
|
||||
// - "env" — canonical. Matches the WASI / TinyGo convention. The
|
||||
// official SDK examples and docs use this name.
|
||||
// - "host" — long-standing alias kept for backward compatibility.
|
||||
// - "orama" — alias added 2026-05-06 after multiple apps intuited the
|
||||
// brand name as the import target and hit cryptic
|
||||
// "module[orama] not instantiated" errors. Cheap insurance:
|
||||
// a few KB of runtime metadata per alias, zero behavioral
|
||||
// cost. Apps SHOULD prefer `env` going forward; `orama` is
|
||||
// supported indefinitely to avoid breaking deployed code.
|
||||
//
|
||||
// All three names resolve to identical function tables — a WASM module
|
||||
// can mix imports across the three with no consequence.
|
||||
func (e *Engine) registerHostModule(ctx context.Context) error {
|
||||
// Register under both "env" and "host" to support different import styles
|
||||
for _, moduleName := range []string{"env", "host"} {
|
||||
for _, moduleName := range []string{"env", "host", "orama"} {
|
||||
_, err := e.runtime.NewHostModuleBuilder(moduleName).
|
||||
NewFunctionBuilder().WithFunc(e.hGetCallerWallet).Export("get_caller_wallet").
|
||||
NewFunctionBuilder().WithFunc(e.hGetWSClientID).Export("get_ws_client_id").
|
||||
|
||||
222
core/pkg/serverless/engine_alias_test.go
Normal file
222
core/pkg/serverless/engine_alias_test.go
Normal file
@ -0,0 +1,222 @@
|
||||
package serverless
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/tetratelabs/wazero"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// TestEngine_HostModule_AllAliases_Registered locks in the
|
||||
// "we register host functions under multiple module names" property.
|
||||
//
|
||||
// Background: WASM modules declare imports as `(import "modulename" "fn" ...)`.
|
||||
// At instantiate time, wazero matches each declared import against host
|
||||
// modules registered with the runtime. If the WASM module imports from a
|
||||
// name we never registered, instantiation fails with the cryptic error
|
||||
// `module[<name>] not instantiated`.
|
||||
//
|
||||
// Three apps and counting have intuited the import name as "orama" instead
|
||||
// of the WASI/TinyGo-canonical "env". To avoid hours of debugging this
|
||||
// every time, we register the same export set under three names:
|
||||
//
|
||||
// - "env" (canonical — matches WASI/TinyGo convention; SDK examples)
|
||||
// - "host" (long-standing alias)
|
||||
// - "orama" (added 2026-05-06 after AnChat hit the wall)
|
||||
//
|
||||
// This test fails if anyone removes one of those aliases — that's an
|
||||
// immediate breaking change for deployed WASM that imports from the
|
||||
// removed name.
|
||||
func TestEngine_HostModule_AllAliases_Registered(t *testing.T) {
|
||||
registry := NewMockRegistry()
|
||||
hostServices := NewMockHostServices()
|
||||
logger := zap.NewNop()
|
||||
|
||||
engine, err := NewEngine(nil, registry, hostServices, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("NewEngine: %v", err)
|
||||
}
|
||||
defer func() { _ = engine.runtime.Close(context.Background()) }()
|
||||
|
||||
// Each alias MUST resolve to an instantiated module. nil means the
|
||||
// host module isn't registered → WASM imports against that name fail.
|
||||
for _, name := range []string{"env", "host", "orama"} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
mod := engine.runtime.Module(name)
|
||||
if mod == nil {
|
||||
t.Fatalf("host module %q is NOT registered — "+
|
||||
"WASM imports from this module name will fail to instantiate",
|
||||
name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestEngine_HostModule_OramaImport_Instantiates is the end-to-end
|
||||
// regression test. It compiles a minimal WASM binary that declares an
|
||||
// import from `orama.log_info` and verifies it instantiates against our
|
||||
// runtime. If the alias regresses, this fails with exactly the error
|
||||
// AnChat hit in production: `module[orama] not instantiated`.
|
||||
//
|
||||
// The WASM binary is hand-crafted from the spec's binary format — keeps
|
||||
// the test self-contained without a TinyGo build step.
|
||||
func TestEngine_HostModule_OramaImport_Instantiates(t *testing.T) {
|
||||
registry := NewMockRegistry()
|
||||
hostServices := NewMockHostServices()
|
||||
logger := zap.NewNop()
|
||||
|
||||
engine, err := NewEngine(nil, registry, hostServices, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("NewEngine: %v", err)
|
||||
}
|
||||
defer func() { _ = engine.runtime.Close(context.Background()) }()
|
||||
|
||||
// Build a minimal valid WASM module that imports `orama.log_info`.
|
||||
// log_info has signature func(levelPtr, levelLen, msgPtr, msgLen uint32),
|
||||
// matching the host registration. wazero's instantiate validates
|
||||
// imports — a missing module name surfaces immediately.
|
||||
// log_info has signature func(ptr, size uint32) → no returns (i32 i32 -> ()).
|
||||
// Matching the host registration matters: a signature mismatch shifts
|
||||
// the failure mode away from the alias-resolution path we're testing.
|
||||
wasm := buildWASMWithImport("orama", "log_info", []byte{0x7f, 0x7f}, nil)
|
||||
|
||||
ctx := context.Background()
|
||||
compiled, err := engine.runtime.CompileModule(ctx, wasm)
|
||||
if err != nil {
|
||||
t.Fatalf("CompileModule: %v", err)
|
||||
}
|
||||
defer func() { _ = compiled.Close(ctx) }()
|
||||
|
||||
// Disable WASI _start so the module can instantiate without WASI imports.
|
||||
mod, err := engine.runtime.InstantiateModule(ctx, compiled,
|
||||
wazero.NewModuleConfig().WithStartFunctions())
|
||||
if err != nil {
|
||||
t.Fatalf("instantiate failed (orama alias regressed?): %v", err)
|
||||
}
|
||||
_ = mod.Close(ctx)
|
||||
}
|
||||
|
||||
// TestEngine_HostModule_HostImport_Instantiates exercises the same end-to-end
|
||||
// path against the "host" alias.
|
||||
func TestEngine_HostModule_HostImport_Instantiates(t *testing.T) {
|
||||
registry := NewMockRegistry()
|
||||
hostServices := NewMockHostServices()
|
||||
logger := zap.NewNop()
|
||||
|
||||
engine, err := NewEngine(nil, registry, hostServices, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("NewEngine: %v", err)
|
||||
}
|
||||
defer func() { _ = engine.runtime.Close(context.Background()) }()
|
||||
|
||||
wasm := buildWASMWithImport("host", "log_info", []byte{0x7f, 0x7f}, nil)
|
||||
ctx := context.Background()
|
||||
compiled, err := engine.runtime.CompileModule(ctx, wasm)
|
||||
if err != nil {
|
||||
t.Fatalf("CompileModule: %v", err)
|
||||
}
|
||||
defer func() { _ = compiled.Close(ctx) }()
|
||||
|
||||
mod, err := engine.runtime.InstantiateModule(ctx, compiled,
|
||||
wazero.NewModuleConfig().WithStartFunctions())
|
||||
if err != nil {
|
||||
t.Fatalf("instantiate failed for host alias: %v", err)
|
||||
}
|
||||
_ = mod.Close(ctx)
|
||||
}
|
||||
|
||||
// TestEngine_HostModule_UnknownImport_Fails confirms the negative case:
|
||||
// importing from a module name we DON'T register fails with the wazero
|
||||
// error format we recognize. If wazero ever changes that format the test
|
||||
// catches it; otherwise it documents the failure mode.
|
||||
func TestEngine_HostModule_UnknownImport_Fails(t *testing.T) {
|
||||
registry := NewMockRegistry()
|
||||
hostServices := NewMockHostServices()
|
||||
logger := zap.NewNop()
|
||||
|
||||
engine, err := NewEngine(nil, registry, hostServices, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("NewEngine: %v", err)
|
||||
}
|
||||
defer func() { _ = engine.runtime.Close(context.Background()) }()
|
||||
|
||||
wasm := buildWASMWithImport("definitely_not_registered", "log_info",
|
||||
[]byte{0x7f, 0x7f}, nil)
|
||||
ctx := context.Background()
|
||||
compiled, err := engine.runtime.CompileModule(ctx, wasm)
|
||||
if err != nil {
|
||||
t.Fatalf("CompileModule: %v", err)
|
||||
}
|
||||
defer func() { _ = compiled.Close(ctx) }()
|
||||
|
||||
_, err = engine.runtime.InstantiateModule(ctx, compiled,
|
||||
wazero.NewModuleConfig().WithStartFunctions())
|
||||
if err == nil {
|
||||
t.Fatal("expected instantiate to fail for unknown module name, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// buildWASMWithImport hand-builds a minimal valid WASM binary that:
|
||||
// - imports a single function from (module, fn)
|
||||
// - has signature: params=paramTypes returns=returnTypes (WASM value-type bytes)
|
||||
//
|
||||
// Returns the binary as a []byte. Used by the alias instantiation tests
|
||||
// to avoid pulling in a TinyGo / wabin dependency.
|
||||
//
|
||||
// Reference: https://webassembly.github.io/spec/core/binary/modules.html
|
||||
func buildWASMWithImport(module, fn string, paramTypes, returnTypes []byte) []byte {
|
||||
// Type section: one function type with `paramTypes -> returnTypes`.
|
||||
typeSec := []byte{
|
||||
0x01, // section ID = type
|
||||
}
|
||||
typeBody := []byte{
|
||||
0x01, // count of types = 1
|
||||
0x60, // func type tag
|
||||
}
|
||||
typeBody = append(typeBody, leb128(uint32(len(paramTypes)))...)
|
||||
typeBody = append(typeBody, paramTypes...)
|
||||
typeBody = append(typeBody, leb128(uint32(len(returnTypes)))...)
|
||||
typeBody = append(typeBody, returnTypes...)
|
||||
typeSec = append(typeSec, leb128(uint32(len(typeBody)))...)
|
||||
typeSec = append(typeSec, typeBody...)
|
||||
|
||||
// Import section: one entry: (module, fn) with kind=function, type-index=0.
|
||||
impBody := []byte{0x01} // count of imports = 1
|
||||
impBody = append(impBody, leb128(uint32(len(module)))...)
|
||||
impBody = append(impBody, module...)
|
||||
impBody = append(impBody, leb128(uint32(len(fn)))...)
|
||||
impBody = append(impBody, fn...)
|
||||
impBody = append(impBody, 0x00, 0x00) // kind=func, type idx=0
|
||||
|
||||
impSec := []byte{0x02}
|
||||
impSec = append(impSec, leb128(uint32(len(impBody)))...)
|
||||
impSec = append(impSec, impBody...)
|
||||
|
||||
// Magic + version.
|
||||
out := []byte{
|
||||
0x00, 0x61, 0x73, 0x6d, // \0asm
|
||||
0x01, 0x00, 0x00, 0x00, // version 1
|
||||
}
|
||||
out = append(out, typeSec...)
|
||||
out = append(out, impSec...)
|
||||
return out
|
||||
}
|
||||
|
||||
// leb128 encodes a uint32 as little-endian base-128 (WASM's canonical
|
||||
// length encoding for sections and counts).
|
||||
func leb128(v uint32) []byte {
|
||||
var out []byte
|
||||
for {
|
||||
b := byte(v & 0x7f)
|
||||
v >>= 7
|
||||
if v != 0 {
|
||||
b |= 0x80
|
||||
}
|
||||
out = append(out, b)
|
||||
if v == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@ -17,6 +17,33 @@
|
||||
// return fn.JSON(map[string]string{"greeting": "Hello, " + req.Name + "!"})
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// # Host functions and WASM imports
|
||||
//
|
||||
// Functions written using only this SDK do NOT need any //go:wasmimport
|
||||
// directives — input flows through stdin and output through stdout, both
|
||||
// of which the runtime provides via WASI.
|
||||
//
|
||||
// If you need direct access to host functions (db_query, pubsub_publish,
|
||||
// http_fetch, etc.) declare them via //go:wasmimport. The CANONICAL host
|
||||
// module name is "env":
|
||||
//
|
||||
// //go:wasmimport env db_query
|
||||
// func dbQuery(queryPtr, queryLen, argsPtr, argsLen uint32) uint64
|
||||
//
|
||||
// For backward compatibility, the runtime ALSO exposes the same export
|
||||
// set under the names "host" and "orama". You may use any of the three
|
||||
// interchangeably — they resolve to identical function tables. New code
|
||||
// should prefer "env" because it matches the WASI/TinyGo convention and
|
||||
// what every example in this SDK uses.
|
||||
//
|
||||
// //go:wasmimport env db_query // canonical (preferred)
|
||||
// //go:wasmimport host db_query // alias, supported indefinitely
|
||||
// //go:wasmimport orama db_query // alias, supported indefinitely
|
||||
//
|
||||
// All three names produce identical runtime behavior. If you see the
|
||||
// runtime error `module[X] not instantiated`, your function imported
|
||||
// from a name other than the three above — fix the directive.
|
||||
package fn
|
||||
|
||||
import (
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user