//go:build js && wasm package wasmdb import ( "bytes" "context" "encoding/json" "fmt" "syscall/js" "git.mleku.dev/mleku/nostr/encoders/event" "git.mleku.dev/mleku/nostr/encoders/filter" "git.mleku.dev/mleku/nostr/encoders/hex" "git.mleku.dev/mleku/nostr/encoders/kind" "git.mleku.dev/mleku/nostr/encoders/tag" "git.mleku.dev/mleku/nostr/encoders/timestamp" ) // JSBridge holds the database instance for JavaScript access var jsBridge *JSBridge // JSBridge wraps the WasmDB instance for JavaScript interop // Exposes a relay protocol interface (NIP-01) rather than direct database access type JSBridge struct { db *W ctx context.Context cancel context.CancelFunc } // RegisterJSBridge exposes the relay protocol API to JavaScript func RegisterJSBridge(db *W, ctx context.Context, cancel context.CancelFunc) { jsBridge = &JSBridge{ db: db, ctx: ctx, cancel: cancel, } // Create the wasmdb global object with relay protocol interface wasmdbObj := map[string]interface{}{ // Lifecycle "isReady": js.FuncOf(jsBridge.jsIsReady), "close": js.FuncOf(jsBridge.jsClose), "wipe": js.FuncOf(jsBridge.jsWipe), // Relay Protocol (NIP-01) // This is the main entry point - handles EVENT, REQ, CLOSE messages "handleMessage": js.FuncOf(jsBridge.jsHandleMessage), // Graph Query Extensions "queryGraph": js.FuncOf(jsBridge.jsQueryGraph), // Marker Extensions (key-value storage via relay protocol) // ["MARKER", "set", key, value] -> ["OK", key, true] // ["MARKER", "get", key] -> ["MARKER", key, value] // ["MARKER", "delete", key] -> ["OK", key, true] // These are also handled via handleMessage } js.Global().Set("wasmdb", wasmdbObj) } // jsIsReady returns true if the database is ready func (b *JSBridge) jsIsReady(this js.Value, args []js.Value) interface{} { select { case <-b.db.Ready(): return true default: return false } } // jsClose closes the database func (b *JSBridge) jsClose(this js.Value, args []js.Value) interface{} { return promiseWrapper(func() (interface{}, error) { err := b.db.Close() return nil, err }) } // jsWipe wipes all data from the database func (b *JSBridge) jsWipe(this js.Value, args []js.Value) interface{} { return promiseWrapper(func() (interface{}, error) { err := b.db.Wipe() return nil, err }) } // jsHandleMessage handles NIP-01 relay protocol messages // Input: JSON string representing a relay message array // // ["EVENT", ] - Submit an event // ["REQ", , ...] - Request events // ["CLOSE", ] - Close a subscription // ["MARKER", "set"|"get"|"delete", key, value?] - Marker operations // // Output: Promise - Array of JSON response messages func (b *JSBridge) jsHandleMessage(this js.Value, args []js.Value) interface{} { if len(args) < 1 { return rejectPromise("handleMessage requires message JSON argument") } messageJSON := args[0].String() return promiseWrapper(func() (interface{}, error) { // Parse the message array var message []json.RawMessage if err := json.Unmarshal([]byte(messageJSON), &message); err != nil { return nil, fmt.Errorf("invalid message format: %w", err) } if len(message) < 1 { return nil, fmt.Errorf("empty message") } // Get message type var msgType string if err := json.Unmarshal(message[0], &msgType); err != nil { return nil, fmt.Errorf("invalid message type: %w", err) } switch msgType { case "EVENT": return b.handleEvent(message) case "REQ": return b.handleReq(message) case "CLOSE": return b.handleClose(message) case "MARKER": return b.handleMarker(message) default: return nil, fmt.Errorf("unknown message type: %s", msgType) } }) } // handleEvent processes an EVENT message // ["EVENT", ] -> ["OK", , true/false, "message"] func (b *JSBridge) handleEvent(message []json.RawMessage) (interface{}, error) { if len(message) < 2 { return []interface{}{makeOK("", false, "missing event")}, nil } // Parse the event ev, err := parseEventFromRawJSON(message[1]) if err != nil { return []interface{}{makeOK("", false, fmt.Sprintf("invalid event: %s", err))}, nil } eventIDHex := hex.Enc(ev.ID) // Save to database replaced, err := b.db.SaveEvent(b.ctx, ev) if err != nil { return []interface{}{makeOK(eventIDHex, false, err.Error())}, nil } var msg string if replaced { msg = "replaced" } else { msg = "saved" } return []interface{}{makeOK(eventIDHex, true, msg)}, nil } // handleReq processes a REQ message // ["REQ", , ...] -> ["EVENT", , ]..., ["EOSE", ] func (b *JSBridge) handleReq(message []json.RawMessage) (interface{}, error) { if len(message) < 2 { return nil, fmt.Errorf("REQ requires subscription ID") } // Get subscription ID var subID string if err := json.Unmarshal(message[1], &subID); err != nil { return nil, fmt.Errorf("invalid subscription ID: %w", err) } // Parse filters (can have multiple) var allEvents []*event.E for i := 2; i < len(message); i++ { f, err := parseFilterFromRawJSON(message[i]) if err != nil { continue } events, err := b.db.QueryEvents(b.ctx, f) if err != nil { continue } allEvents = append(allEvents, events...) } // Build response messages responses := make([]interface{}, 0, len(allEvents)+1) // Add EVENT messages for _, ev := range allEvents { eventJSON, err := eventToJSON(ev) if err != nil { continue } responses = append(responses, makeEvent(subID, string(eventJSON))) } // Add EOSE responses = append(responses, makeEOSE(subID)) return responses, nil } // handleClose processes a CLOSE message // ["CLOSE", ] -> (no response for local relay) func (b *JSBridge) handleClose(message []json.RawMessage) (interface{}, error) { // For the local relay, subscriptions are stateless (single query/response) // CLOSE is a no-op but we acknowledge it return []interface{}{}, nil } // handleMarker processes MARKER extension messages // ["MARKER", "set", key, value] -> ["OK", key, true] // ["MARKER", "get", key] -> ["MARKER", key, value] or ["MARKER", key, null] // ["MARKER", "delete", key] -> ["OK", key, true] func (b *JSBridge) handleMarker(message []json.RawMessage) (interface{}, error) { if len(message) < 3 { return nil, fmt.Errorf("MARKER requires operation and key") } var operation string if err := json.Unmarshal(message[1], &operation); err != nil { return nil, fmt.Errorf("invalid marker operation: %w", err) } var key string if err := json.Unmarshal(message[2], &key); err != nil { return nil, fmt.Errorf("invalid marker key: %w", err) } switch operation { case "set": if len(message) < 4 { return nil, fmt.Errorf("MARKER set requires value") } var value string if err := json.Unmarshal(message[3], &value); err != nil { return nil, fmt.Errorf("invalid marker value: %w", err) } if err := b.db.SetMarker(key, []byte(value)); err != nil { return []interface{}{makeMarkerOK(key, false, err.Error())}, nil } return []interface{}{makeMarkerOK(key, true, "")}, nil case "get": value, err := b.db.GetMarker(key) if err != nil || value == nil { return []interface{}{makeMarkerResult(key, nil)}, nil } valueStr := string(value) return []interface{}{makeMarkerResult(key, &valueStr)}, nil case "delete": if err := b.db.DeleteMarker(key); err != nil { return []interface{}{makeMarkerOK(key, false, err.Error())}, nil } return []interface{}{makeMarkerOK(key, true, "")}, nil case "has": has := b.db.HasMarker(key) return []interface{}{makeMarkerHas(key, has)}, nil default: return nil, fmt.Errorf("unknown marker operation: %s", operation) } } // jsQueryGraph handles graph query extensions // Args: [queryJSON: string] - JSON-encoded graph query // Returns: Promise - JSON-encoded graph result func (b *JSBridge) jsQueryGraph(this js.Value, args []js.Value) interface{} { if len(args) < 1 { return rejectPromise("queryGraph requires query JSON argument") } queryJSON := args[0].String() return promiseWrapper(func() (interface{}, error) { var query struct { Type string `json:"type"` Pubkey string `json:"pubkey"` Depth int `json:"depth,omitempty"` Limit int `json:"limit,omitempty"` } if err := json.Unmarshal([]byte(queryJSON), &query); err != nil { return nil, fmt.Errorf("invalid graph query: %w", err) } // Set defaults if query.Depth == 0 { query.Depth = 1 } if query.Limit == 0 { query.Limit = 1000 } switch query.Type { case "follows": return b.queryFollows(query.Pubkey, query.Depth, query.Limit) case "followers": return b.queryFollowers(query.Pubkey, query.Limit) case "mutes": return b.queryMutes(query.Pubkey) default: return nil, fmt.Errorf("unknown graph query type: %s", query.Type) } }) } // queryFollows returns who a pubkey follows func (b *JSBridge) queryFollows(pubkeyHex string, depth, limit int) (interface{}, error) { // Query kind 3 (contact list) for the pubkey f := &filter.F{ Kinds: kind.NewWithCap(1), } f.Kinds.K = append(f.Kinds.K, kind.New(3)) f.Authors = tag.NewWithCap(1) f.Authors.T = append(f.Authors.T, []byte(pubkeyHex)) one := uint(1) f.Limit = &one events, err := b.db.QueryEvents(b.ctx, f) if err != nil { return nil, err } var follows []string if len(events) > 0 && events[0].Tags != nil { for _, t := range *events[0].Tags { if t != nil && len(t.T) >= 2 && string(t.T[0]) == "p" { follows = append(follows, string(t.T[1])) } } } result := map[string]interface{}{ "nodes": follows, } jsonBytes, err := json.Marshal(result) if err != nil { return nil, err } return string(jsonBytes), nil } // queryFollowers returns who follows a pubkey func (b *JSBridge) queryFollowers(pubkeyHex string, limit int) (interface{}, error) { // Query kind 3 events that tag this pubkey f := &filter.F{ Kinds: kind.NewWithCap(1), Tags: tag.NewSWithCap(1), } f.Kinds.K = append(f.Kinds.K, kind.New(3)) // Add #p tag filter pTag := tag.NewWithCap(2) pTag.T = append(pTag.T, []byte("p")) pTag.T = append(pTag.T, []byte(pubkeyHex)) f.Tags.Append(pTag) lim := uint(limit) f.Limit = &lim events, err := b.db.QueryEvents(b.ctx, f) if err != nil { return nil, err } var followers []string for _, ev := range events { followers = append(followers, hex.Enc(ev.Pubkey)) } result := map[string]interface{}{ "nodes": followers, } jsonBytes, err := json.Marshal(result) if err != nil { return nil, err } return string(jsonBytes), nil } // queryMutes returns who a pubkey has muted func (b *JSBridge) queryMutes(pubkeyHex string) (interface{}, error) { // Query kind 10000 (mute list) for the pubkey f := &filter.F{ Kinds: kind.NewWithCap(1), } f.Kinds.K = append(f.Kinds.K, kind.New(10000)) f.Authors = tag.NewWithCap(1) f.Authors.T = append(f.Authors.T, []byte(pubkeyHex)) one := uint(1) f.Limit = &one events, err := b.db.QueryEvents(b.ctx, f) if err != nil { return nil, err } var mutes []string if len(events) > 0 && events[0].Tags != nil { for _, t := range *events[0].Tags { if t != nil && len(t.T) >= 2 && string(t.T[0]) == "p" { mutes = append(mutes, string(t.T[1])) } } } result := map[string]interface{}{ "nodes": mutes, } jsonBytes, err := json.Marshal(result) if err != nil { return nil, err } return string(jsonBytes), nil } // Response message builders func makeOK(eventID string, accepted bool, message string) string { msg := []interface{}{"OK", eventID, accepted, message} jsonBytes, _ := json.Marshal(msg) return string(jsonBytes) } func makeEvent(subID, eventJSON string) string { // We return the raw event JSON embedded in the array return fmt.Sprintf(`["EVENT","%s",%s]`, subID, eventJSON) } func makeEOSE(subID string) string { msg := []interface{}{"EOSE", subID} jsonBytes, _ := json.Marshal(msg) return string(jsonBytes) } func makeMarkerOK(key string, success bool, message string) string { msg := []interface{}{"OK", key, success} if message != "" { msg = append(msg, message) } jsonBytes, _ := json.Marshal(msg) return string(jsonBytes) } func makeMarkerResult(key string, value *string) string { var msg []interface{} if value == nil { msg = []interface{}{"MARKER", key, nil} } else { msg = []interface{}{"MARKER", key, *value} } jsonBytes, _ := json.Marshal(msg) return string(jsonBytes) } func makeMarkerHas(key string, has bool) string { msg := []interface{}{"MARKER", key, has} jsonBytes, _ := json.Marshal(msg) return string(jsonBytes) } // Helper functions // promiseWrapper wraps a function in a JavaScript Promise func promiseWrapper(fn func() (interface{}, error)) interface{} { handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} { resolve := args[0] reject := args[1] go func() { result, err := fn() if err != nil { reject.Invoke(err.Error()) } else { resolve.Invoke(result) } }() return nil }) promiseConstructor := js.Global().Get("Promise") return promiseConstructor.New(handler) } // rejectPromise creates a rejected promise with an error message func rejectPromise(msg string) interface{} { handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} { reject := args[1] reject.Invoke(msg) return nil }) promiseConstructor := js.Global().Get("Promise") return promiseConstructor.New(handler) } // parseEventFromRawJSON parses a Nostr event from raw JSON func parseEventFromRawJSON(raw json.RawMessage) (*event.E, error) { return parseEventFromJSON(string(raw)) } // parseEventFromJSON parses a Nostr event from JSON func parseEventFromJSON(jsonStr string) (*event.E, error) { // Parse into intermediate struct for JSON compatibility var raw struct { ID string `json:"id"` Pubkey string `json:"pubkey"` CreatedAt int64 `json:"created_at"` Kind int `json:"kind"` Tags [][]string `json:"tags"` Content string `json:"content"` Sig string `json:"sig"` } if err := json.Unmarshal([]byte(jsonStr), &raw); err != nil { return nil, err } ev := &event.E{ Kind: uint16(raw.Kind), CreatedAt: raw.CreatedAt, Content: []byte(raw.Content), } // Decode ID if id, err := hex.Dec(raw.ID); err == nil && len(id) == 32 { ev.ID = id } // Decode Pubkey if pk, err := hex.Dec(raw.Pubkey); err == nil && len(pk) == 32 { ev.Pubkey = pk } // Decode Sig if sig, err := hex.Dec(raw.Sig); err == nil && len(sig) == 64 { ev.Sig = sig } // Convert tags if len(raw.Tags) > 0 { ev.Tags = tag.NewSWithCap(len(raw.Tags)) for _, t := range raw.Tags { tagBytes := make([][]byte, len(t)) for i, s := range t { tagBytes[i] = []byte(s) } newTag := tag.NewFromBytesSlice(tagBytes...) ev.Tags.Append(newTag) } } return ev, nil } // parseFilterFromRawJSON parses a Nostr filter from raw JSON func parseFilterFromRawJSON(raw json.RawMessage) (*filter.F, error) { return parseFilterFromJSON(string(raw)) } // parseFilterFromJSON parses a Nostr filter from JSON func parseFilterFromJSON(jsonStr string) (*filter.F, error) { // Parse into intermediate struct var raw struct { IDs []string `json:"ids,omitempty"` Authors []string `json:"authors,omitempty"` Kinds []int `json:"kinds,omitempty"` Since *int64 `json:"since,omitempty"` Until *int64 `json:"until,omitempty"` Limit *uint `json:"limit,omitempty"` Search *string `json:"search,omitempty"` } if err := json.Unmarshal([]byte(jsonStr), &raw); err != nil { return nil, err } f := &filter.F{} // Set IDs if len(raw.IDs) > 0 { f.Ids = tag.NewWithCap(len(raw.IDs)) for _, idHex := range raw.IDs { f.Ids.T = append(f.Ids.T, []byte(idHex)) } } // Set Authors if len(raw.Authors) > 0 { f.Authors = tag.NewWithCap(len(raw.Authors)) for _, pkHex := range raw.Authors { f.Authors.T = append(f.Authors.T, []byte(pkHex)) } } // Set Kinds if len(raw.Kinds) > 0 { f.Kinds = kind.NewWithCap(len(raw.Kinds)) for _, k := range raw.Kinds { f.Kinds.K = append(f.Kinds.K, kind.New(uint16(k))) } } // Set timestamps if raw.Since != nil { f.Since = timestamp.New(*raw.Since) } if raw.Until != nil { f.Until = timestamp.New(*raw.Until) } // Set limit if raw.Limit != nil { f.Limit = raw.Limit } // Set search if raw.Search != nil { f.Search = []byte(*raw.Search) } // Handle tag filters (e.g., #e, #p, #t) var rawMap map[string]interface{} json.Unmarshal([]byte(jsonStr), &rawMap) for key, val := range rawMap { if len(key) == 2 && key[0] == '#' { if arr, ok := val.([]interface{}); ok { tagFilter := tag.NewWithCap(len(arr) + 1) // First element is the tag name (e.g., "e", "p") tagFilter.T = append(tagFilter.T, []byte{key[1]}) for _, v := range arr { if s, ok := v.(string); ok { tagFilter.T = append(tagFilter.T, []byte(s)) } } if f.Tags == nil { f.Tags = tag.NewSWithCap(4) } f.Tags.Append(tagFilter) } } } return f, nil } // eventToJSON converts a Nostr event to JSON func eventToJSON(ev *event.E) ([]byte, error) { // Build tags array var tags [][]string if ev.Tags != nil { for _, t := range *ev.Tags { if t == nil { continue } tagStrs := make([]string, len(t.T)) for i, elem := range t.T { tagStrs[i] = string(elem) } tags = append(tags, tagStrs) } } raw := struct { ID string `json:"id"` Pubkey string `json:"pubkey"` CreatedAt int64 `json:"created_at"` Kind int `json:"kind"` Tags [][]string `json:"tags"` Content string `json:"content"` Sig string `json:"sig"` }{ ID: hex.Enc(ev.ID), Pubkey: hex.Enc(ev.Pubkey), CreatedAt: ev.CreatedAt, Kind: int(ev.Kind), Tags: tags, Content: string(ev.Content), Sig: hex.Enc(ev.Sig), } buf := new(bytes.Buffer) enc := json.NewEncoder(buf) enc.SetEscapeHTML(false) if err := enc.Encode(raw); err != nil { return nil, err } // Remove trailing newline from encoder result := buf.Bytes() if len(result) > 0 && result[len(result)-1] == '\n' { result = result[:len(result)-1] } return result, nil }