jsbridge.go raw
1 //go:build js && wasm
2
3 package wasmdb
4
5 import (
6 "bytes"
7 "context"
8 "encoding/json"
9 "fmt"
10 "syscall/js"
11
12 "next.orly.dev/pkg/nostr/encoders/event"
13 "next.orly.dev/pkg/nostr/encoders/filter"
14 "next.orly.dev/pkg/nostr/encoders/hex"
15 "next.orly.dev/pkg/nostr/encoders/kind"
16 "next.orly.dev/pkg/nostr/encoders/tag"
17 "next.orly.dev/pkg/nostr/encoders/timestamp"
18 )
19
20 // JSBridge holds the database instance for JavaScript access
21 var jsBridge *JSBridge
22
23 // JSBridge wraps the WasmDB instance for JavaScript interop
24 // Exposes a relay protocol interface (NIP-01) rather than direct database access
25 type JSBridge struct {
26 db *W
27 ctx context.Context
28 cancel context.CancelFunc
29 }
30
31 // RegisterJSBridge exposes the relay protocol API to JavaScript
32 func RegisterJSBridge(db *W, ctx context.Context, cancel context.CancelFunc) {
33 jsBridge = &JSBridge{
34 db: db,
35 ctx: ctx,
36 cancel: cancel,
37 }
38
39 // Create the wasmdb global object with relay protocol interface
40 wasmdbObj := map[string]interface{}{
41 // Lifecycle
42 "isReady": js.FuncOf(jsBridge.jsIsReady),
43 "close": js.FuncOf(jsBridge.jsClose),
44 "wipe": js.FuncOf(jsBridge.jsWipe),
45
46 // Relay Protocol (NIP-01)
47 // This is the main entry point - handles EVENT, REQ, CLOSE messages
48 "handleMessage": js.FuncOf(jsBridge.jsHandleMessage),
49
50 // Graph Query Extensions
51 "queryGraph": js.FuncOf(jsBridge.jsQueryGraph),
52
53 // Marker Extensions (key-value storage via relay protocol)
54 // ["MARKER", "set", key, value] -> ["OK", key, true]
55 // ["MARKER", "get", key] -> ["MARKER", key, value]
56 // ["MARKER", "delete", key] -> ["OK", key, true]
57 // These are also handled via handleMessage
58 }
59
60 js.Global().Set("wasmdb", wasmdbObj)
61 }
62
63 // jsIsReady returns true if the database is ready
64 func (b *JSBridge) jsIsReady(this js.Value, args []js.Value) interface{} {
65 select {
66 case <-b.db.Ready():
67 return true
68 default:
69 return false
70 }
71 }
72
73 // jsClose closes the database
74 func (b *JSBridge) jsClose(this js.Value, args []js.Value) interface{} {
75 return promiseWrapper(func() (interface{}, error) {
76 err := b.db.Close()
77 return nil, err
78 })
79 }
80
81 // jsWipe wipes all data from the database
82 func (b *JSBridge) jsWipe(this js.Value, args []js.Value) interface{} {
83 return promiseWrapper(func() (interface{}, error) {
84 err := b.db.Wipe()
85 return nil, err
86 })
87 }
88
89 // jsHandleMessage handles NIP-01 relay protocol messages
90 // Input: JSON string representing a relay message array
91 //
92 // ["EVENT", <event>] - Submit an event
93 // ["REQ", <sub_id>, <filter>...] - Request events
94 // ["CLOSE", <sub_id>] - Close a subscription
95 // ["MARKER", "set"|"get"|"delete", key, value?] - Marker operations
96 //
97 // Output: Promise<string[]> - Array of JSON response messages
98 func (b *JSBridge) jsHandleMessage(this js.Value, args []js.Value) interface{} {
99 if len(args) < 1 {
100 return rejectPromise("handleMessage requires message JSON argument")
101 }
102
103 messageJSON := args[0].String()
104
105 return promiseWrapper(func() (interface{}, error) {
106 // Parse the message array
107 var message []json.RawMessage
108 if err := json.Unmarshal([]byte(messageJSON), &message); err != nil {
109 return nil, fmt.Errorf("invalid message format: %w", err)
110 }
111
112 if len(message) < 1 {
113 return nil, fmt.Errorf("empty message")
114 }
115
116 // Get message type
117 var msgType string
118 if err := json.Unmarshal(message[0], &msgType); err != nil {
119 return nil, fmt.Errorf("invalid message type: %w", err)
120 }
121
122 switch msgType {
123 case "EVENT":
124 return b.handleEvent(message)
125 case "REQ":
126 return b.handleReq(message)
127 case "CLOSE":
128 return b.handleClose(message)
129 case "MARKER":
130 return b.handleMarker(message)
131 default:
132 return nil, fmt.Errorf("unknown message type: %s", msgType)
133 }
134 })
135 }
136
137 // handleEvent processes an EVENT message
138 // ["EVENT", <event>] -> ["OK", <id>, true/false, "message"]
139 func (b *JSBridge) handleEvent(message []json.RawMessage) (interface{}, error) {
140 if len(message) < 2 {
141 return []interface{}{makeOK("", false, "missing event")}, nil
142 }
143
144 // Parse the event
145 ev, err := parseEventFromRawJSON(message[1])
146 if err != nil {
147 return []interface{}{makeOK("", false, fmt.Sprintf("invalid event: %s", err))}, nil
148 }
149
150 eventIDHex := hex.Enc(ev.ID)
151
152 // Save to database
153 replaced, err := b.db.SaveEvent(b.ctx, ev)
154 if err != nil {
155 return []interface{}{makeOK(eventIDHex, false, err.Error())}, nil
156 }
157
158 var msg string
159 if replaced {
160 msg = "replaced"
161 } else {
162 msg = "saved"
163 }
164
165 return []interface{}{makeOK(eventIDHex, true, msg)}, nil
166 }
167
168 // handleReq processes a REQ message
169 // ["REQ", <sub_id>, <filter>...] -> ["EVENT", <sub_id>, <event>]..., ["EOSE", <sub_id>]
170 func (b *JSBridge) handleReq(message []json.RawMessage) (interface{}, error) {
171 if len(message) < 2 {
172 return nil, fmt.Errorf("REQ requires subscription ID")
173 }
174
175 // Get subscription ID
176 var subID string
177 if err := json.Unmarshal(message[1], &subID); err != nil {
178 return nil, fmt.Errorf("invalid subscription ID: %w", err)
179 }
180
181 // Parse filters (can have multiple)
182 var allEvents []*event.E
183 for i := 2; i < len(message); i++ {
184 f, err := parseFilterFromRawJSON(message[i])
185 if err != nil {
186 continue
187 }
188
189 events, err := b.db.QueryEvents(b.ctx, f)
190 if err != nil {
191 continue
192 }
193
194 allEvents = append(allEvents, events...)
195 }
196
197 // Build response messages
198 responses := make([]interface{}, 0, len(allEvents)+1)
199
200 // Add EVENT messages
201 for _, ev := range allEvents {
202 eventJSON, err := eventToJSON(ev)
203 if err != nil {
204 continue
205 }
206 responses = append(responses, makeEvent(subID, string(eventJSON)))
207 }
208
209 // Add EOSE
210 responses = append(responses, makeEOSE(subID))
211
212 return responses, nil
213 }
214
215 // handleClose processes a CLOSE message
216 // ["CLOSE", <sub_id>] -> (no response for local relay)
217 func (b *JSBridge) handleClose(message []json.RawMessage) (interface{}, error) {
218 // For the local relay, subscriptions are stateless (single query/response)
219 // CLOSE is a no-op but we acknowledge it
220 return []interface{}{}, nil
221 }
222
223 // handleMarker processes MARKER extension messages
224 // ["MARKER", "set", key, value] -> ["OK", key, true]
225 // ["MARKER", "get", key] -> ["MARKER", key, value] or ["MARKER", key, null]
226 // ["MARKER", "delete", key] -> ["OK", key, true]
227 func (b *JSBridge) handleMarker(message []json.RawMessage) (interface{}, error) {
228 if len(message) < 3 {
229 return nil, fmt.Errorf("MARKER requires operation and key")
230 }
231
232 var operation string
233 if err := json.Unmarshal(message[1], &operation); err != nil {
234 return nil, fmt.Errorf("invalid marker operation: %w", err)
235 }
236
237 var key string
238 if err := json.Unmarshal(message[2], &key); err != nil {
239 return nil, fmt.Errorf("invalid marker key: %w", err)
240 }
241
242 switch operation {
243 case "set":
244 if len(message) < 4 {
245 return nil, fmt.Errorf("MARKER set requires value")
246 }
247 var value string
248 if err := json.Unmarshal(message[3], &value); err != nil {
249 return nil, fmt.Errorf("invalid marker value: %w", err)
250 }
251 if err := b.db.SetMarker(key, []byte(value)); err != nil {
252 return []interface{}{makeMarkerOK(key, false, err.Error())}, nil
253 }
254 return []interface{}{makeMarkerOK(key, true, "")}, nil
255
256 case "get":
257 value, err := b.db.GetMarker(key)
258 if err != nil || value == nil {
259 return []interface{}{makeMarkerResult(key, nil)}, nil
260 }
261 valueStr := string(value)
262 return []interface{}{makeMarkerResult(key, &valueStr)}, nil
263
264 case "delete":
265 if err := b.db.DeleteMarker(key); err != nil {
266 return []interface{}{makeMarkerOK(key, false, err.Error())}, nil
267 }
268 return []interface{}{makeMarkerOK(key, true, "")}, nil
269
270 case "has":
271 has := b.db.HasMarker(key)
272 return []interface{}{makeMarkerHas(key, has)}, nil
273
274 default:
275 return nil, fmt.Errorf("unknown marker operation: %s", operation)
276 }
277 }
278
279 // jsQueryGraph handles graph query extensions
280 // Args: [queryJSON: string] - JSON-encoded graph query
281 // Returns: Promise<string> - JSON-encoded graph result
282 func (b *JSBridge) jsQueryGraph(this js.Value, args []js.Value) interface{} {
283 if len(args) < 1 {
284 return rejectPromise("queryGraph requires query JSON argument")
285 }
286
287 queryJSON := args[0].String()
288
289 return promiseWrapper(func() (interface{}, error) {
290 var query struct {
291 Type string `json:"type"`
292 Pubkey string `json:"pubkey"`
293 Depth int `json:"depth,omitempty"`
294 Limit int `json:"limit,omitempty"`
295 }
296
297 if err := json.Unmarshal([]byte(queryJSON), &query); err != nil {
298 return nil, fmt.Errorf("invalid graph query: %w", err)
299 }
300
301 // Set defaults
302 if query.Depth == 0 {
303 query.Depth = 1
304 }
305 if query.Limit == 0 {
306 query.Limit = 1000
307 }
308
309 switch query.Type {
310 case "follows":
311 return b.queryFollows(query.Pubkey, query.Depth, query.Limit)
312 case "followers":
313 return b.queryFollowers(query.Pubkey, query.Limit)
314 case "mutes":
315 return b.queryMutes(query.Pubkey)
316 default:
317 return nil, fmt.Errorf("unknown graph query type: %s", query.Type)
318 }
319 })
320 }
321
322 // queryFollows returns who a pubkey follows
323 func (b *JSBridge) queryFollows(pubkeyHex string, depth, limit int) (interface{}, error) {
324 // Query kind 3 (contact list) for the pubkey
325 f := &filter.F{
326 Kinds: kind.NewWithCap(1),
327 }
328 f.Kinds.K = append(f.Kinds.K, kind.New(3))
329 f.Authors = tag.NewWithCap(1)
330 f.Authors.T = append(f.Authors.T, []byte(pubkeyHex))
331 one := uint(1)
332 f.Limit = &one
333
334 events, err := b.db.QueryEvents(b.ctx, f)
335 if err != nil {
336 return nil, err
337 }
338
339 var follows []string
340 if len(events) > 0 && events[0].Tags != nil {
341 for _, t := range *events[0].Tags {
342 if t != nil && len(t.T) >= 2 && string(t.T[0]) == "p" {
343 follows = append(follows, string(t.T[1]))
344 }
345 }
346 }
347
348 result := map[string]interface{}{
349 "nodes": follows,
350 }
351 jsonBytes, err := json.Marshal(result)
352 if err != nil {
353 return nil, err
354 }
355 return string(jsonBytes), nil
356 }
357
358 // queryFollowers returns who follows a pubkey
359 func (b *JSBridge) queryFollowers(pubkeyHex string, limit int) (interface{}, error) {
360 // Query kind 3 events that tag this pubkey
361 f := &filter.F{
362 Kinds: kind.NewWithCap(1),
363 Tags: tag.NewSWithCap(1),
364 }
365 f.Kinds.K = append(f.Kinds.K, kind.New(3))
366
367 // Add #p tag filter
368 pTag := tag.NewWithCap(2)
369 pTag.T = append(pTag.T, []byte("p"))
370 pTag.T = append(pTag.T, []byte(pubkeyHex))
371 f.Tags.Append(pTag)
372
373 lim := uint(limit)
374 f.Limit = &lim
375
376 events, err := b.db.QueryEvents(b.ctx, f)
377 if err != nil {
378 return nil, err
379 }
380
381 var followers []string
382 for _, ev := range events {
383 followers = append(followers, hex.Enc(ev.Pubkey))
384 }
385
386 result := map[string]interface{}{
387 "nodes": followers,
388 }
389 jsonBytes, err := json.Marshal(result)
390 if err != nil {
391 return nil, err
392 }
393 return string(jsonBytes), nil
394 }
395
396 // queryMutes returns who a pubkey has muted
397 func (b *JSBridge) queryMutes(pubkeyHex string) (interface{}, error) {
398 // Query kind 10000 (mute list) for the pubkey
399 f := &filter.F{
400 Kinds: kind.NewWithCap(1),
401 }
402 f.Kinds.K = append(f.Kinds.K, kind.New(10000))
403 f.Authors = tag.NewWithCap(1)
404 f.Authors.T = append(f.Authors.T, []byte(pubkeyHex))
405 one := uint(1)
406 f.Limit = &one
407
408 events, err := b.db.QueryEvents(b.ctx, f)
409 if err != nil {
410 return nil, err
411 }
412
413 var mutes []string
414 if len(events) > 0 && events[0].Tags != nil {
415 for _, t := range *events[0].Tags {
416 if t != nil && len(t.T) >= 2 && string(t.T[0]) == "p" {
417 mutes = append(mutes, string(t.T[1]))
418 }
419 }
420 }
421
422 result := map[string]interface{}{
423 "nodes": mutes,
424 }
425 jsonBytes, err := json.Marshal(result)
426 if err != nil {
427 return nil, err
428 }
429 return string(jsonBytes), nil
430 }
431
432 // Response message builders
433
434 func makeOK(eventID string, accepted bool, message string) string {
435 msg := []interface{}{"OK", eventID, accepted, message}
436 jsonBytes, _ := json.Marshal(msg)
437 return string(jsonBytes)
438 }
439
440 func makeEvent(subID, eventJSON string) string {
441 // We return the raw event JSON embedded in the array
442 return fmt.Sprintf(`["EVENT","%s",%s]`, subID, eventJSON)
443 }
444
445 func makeEOSE(subID string) string {
446 msg := []interface{}{"EOSE", subID}
447 jsonBytes, _ := json.Marshal(msg)
448 return string(jsonBytes)
449 }
450
451 func makeMarkerOK(key string, success bool, message string) string {
452 msg := []interface{}{"OK", key, success}
453 if message != "" {
454 msg = append(msg, message)
455 }
456 jsonBytes, _ := json.Marshal(msg)
457 return string(jsonBytes)
458 }
459
460 func makeMarkerResult(key string, value *string) string {
461 var msg []interface{}
462 if value == nil {
463 msg = []interface{}{"MARKER", key, nil}
464 } else {
465 msg = []interface{}{"MARKER", key, *value}
466 }
467 jsonBytes, _ := json.Marshal(msg)
468 return string(jsonBytes)
469 }
470
471 func makeMarkerHas(key string, has bool) string {
472 msg := []interface{}{"MARKER", key, has}
473 jsonBytes, _ := json.Marshal(msg)
474 return string(jsonBytes)
475 }
476
477 // Helper functions
478
479 // promiseWrapper wraps a function in a JavaScript Promise
480 func promiseWrapper(fn func() (interface{}, error)) interface{} {
481 handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
482 resolve := args[0]
483 reject := args[1]
484
485 go func() {
486 result, err := fn()
487 if err != nil {
488 reject.Invoke(err.Error())
489 } else {
490 resolve.Invoke(result)
491 }
492 }()
493
494 return nil
495 })
496
497 promiseConstructor := js.Global().Get("Promise")
498 return promiseConstructor.New(handler)
499 }
500
501 // rejectPromise creates a rejected promise with an error message
502 func rejectPromise(msg string) interface{} {
503 handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
504 reject := args[1]
505 reject.Invoke(msg)
506 return nil
507 })
508
509 promiseConstructor := js.Global().Get("Promise")
510 return promiseConstructor.New(handler)
511 }
512
513 // parseEventFromRawJSON parses a Nostr event from raw JSON
514 func parseEventFromRawJSON(raw json.RawMessage) (*event.E, error) {
515 return parseEventFromJSON(string(raw))
516 }
517
518 // parseEventFromJSON parses a Nostr event from JSON
519 func parseEventFromJSON(jsonStr string) (*event.E, error) {
520 // Parse into intermediate struct for JSON compatibility
521 var raw struct {
522 ID string `json:"id"`
523 Pubkey string `json:"pubkey"`
524 CreatedAt int64 `json:"created_at"`
525 Kind int `json:"kind"`
526 Tags [][]string `json:"tags"`
527 Content string `json:"content"`
528 Sig string `json:"sig"`
529 }
530
531 if err := json.Unmarshal([]byte(jsonStr), &raw); err != nil {
532 return nil, err
533 }
534
535 ev := &event.E{
536 Kind: uint16(raw.Kind),
537 CreatedAt: raw.CreatedAt,
538 Content: []byte(raw.Content),
539 }
540
541 // Decode ID
542 if id, err := hex.Dec(raw.ID); err == nil && len(id) == 32 {
543 ev.ID = id
544 }
545
546 // Decode Pubkey
547 if pk, err := hex.Dec(raw.Pubkey); err == nil && len(pk) == 32 {
548 ev.Pubkey = pk
549 }
550
551 // Decode Sig
552 if sig, err := hex.Dec(raw.Sig); err == nil && len(sig) == 64 {
553 ev.Sig = sig
554 }
555
556 // Convert tags
557 if len(raw.Tags) > 0 {
558 ev.Tags = tag.NewSWithCap(len(raw.Tags))
559 for _, t := range raw.Tags {
560 tagBytes := make([][]byte, len(t))
561 for i, s := range t {
562 tagBytes[i] = []byte(s)
563 }
564 newTag := tag.NewFromBytesSlice(tagBytes...)
565 ev.Tags.Append(newTag)
566 }
567 }
568
569 return ev, nil
570 }
571
572 // parseFilterFromRawJSON parses a Nostr filter from raw JSON
573 func parseFilterFromRawJSON(raw json.RawMessage) (*filter.F, error) {
574 return parseFilterFromJSON(string(raw))
575 }
576
577 // parseFilterFromJSON parses a Nostr filter from JSON
578 func parseFilterFromJSON(jsonStr string) (*filter.F, error) {
579 // Parse into intermediate struct
580 var raw struct {
581 IDs []string `json:"ids,omitempty"`
582 Authors []string `json:"authors,omitempty"`
583 Kinds []int `json:"kinds,omitempty"`
584 Since *int64 `json:"since,omitempty"`
585 Until *int64 `json:"until,omitempty"`
586 Limit *uint `json:"limit,omitempty"`
587 Search *string `json:"search,omitempty"`
588 }
589
590 if err := json.Unmarshal([]byte(jsonStr), &raw); err != nil {
591 return nil, err
592 }
593
594 f := &filter.F{}
595
596 // Set IDs
597 if len(raw.IDs) > 0 {
598 f.Ids = tag.NewWithCap(len(raw.IDs))
599 for _, idHex := range raw.IDs {
600 f.Ids.T = append(f.Ids.T, []byte(idHex))
601 }
602 }
603
604 // Set Authors
605 if len(raw.Authors) > 0 {
606 f.Authors = tag.NewWithCap(len(raw.Authors))
607 for _, pkHex := range raw.Authors {
608 f.Authors.T = append(f.Authors.T, []byte(pkHex))
609 }
610 }
611
612 // Set Kinds
613 if len(raw.Kinds) > 0 {
614 f.Kinds = kind.NewWithCap(len(raw.Kinds))
615 for _, k := range raw.Kinds {
616 f.Kinds.K = append(f.Kinds.K, kind.New(uint16(k)))
617 }
618 }
619
620 // Set timestamps
621 if raw.Since != nil {
622 f.Since = timestamp.New(*raw.Since)
623 }
624 if raw.Until != nil {
625 f.Until = timestamp.New(*raw.Until)
626 }
627
628 // Set limit
629 if raw.Limit != nil {
630 f.Limit = raw.Limit
631 }
632
633 // Set search
634 if raw.Search != nil {
635 f.Search = []byte(*raw.Search)
636 }
637
638 // Handle tag filters (e.g., #e, #p, #t)
639 var rawMap map[string]interface{}
640 json.Unmarshal([]byte(jsonStr), &rawMap)
641 for key, val := range rawMap {
642 if len(key) == 2 && key[0] == '#' {
643 if arr, ok := val.([]interface{}); ok {
644 tagFilter := tag.NewWithCap(len(arr) + 1)
645 // First element is the tag name (e.g., "e", "p")
646 tagFilter.T = append(tagFilter.T, []byte{key[1]})
647 for _, v := range arr {
648 if s, ok := v.(string); ok {
649 tagFilter.T = append(tagFilter.T, []byte(s))
650 }
651 }
652 if f.Tags == nil {
653 f.Tags = tag.NewSWithCap(4)
654 }
655 f.Tags.Append(tagFilter)
656 }
657 }
658 }
659
660 return f, nil
661 }
662
663 // eventToJSON converts a Nostr event to JSON
664 func eventToJSON(ev *event.E) ([]byte, error) {
665 // Build tags array
666 var tags [][]string
667 if ev.Tags != nil {
668 for _, t := range *ev.Tags {
669 if t == nil {
670 continue
671 }
672 tagStrs := make([]string, len(t.T))
673 for i, elem := range t.T {
674 tagStrs[i] = string(elem)
675 }
676 tags = append(tags, tagStrs)
677 }
678 }
679
680 raw := struct {
681 ID string `json:"id"`
682 Pubkey string `json:"pubkey"`
683 CreatedAt int64 `json:"created_at"`
684 Kind int `json:"kind"`
685 Tags [][]string `json:"tags"`
686 Content string `json:"content"`
687 Sig string `json:"sig"`
688 }{
689 ID: hex.Enc(ev.ID),
690 Pubkey: hex.Enc(ev.Pubkey),
691 CreatedAt: ev.CreatedAt,
692 Kind: int(ev.Kind),
693 Tags: tags,
694 Content: string(ev.Content),
695 Sig: hex.Enc(ev.Sig),
696 }
697
698 buf := new(bytes.Buffer)
699 enc := json.NewEncoder(buf)
700 enc.SetEscapeHTML(false)
701 if err := enc.Encode(raw); err != nil {
702 return nil, err
703 }
704
705 // Remove trailing newline from encoder
706 result := buf.Bytes()
707 if len(result) > 0 && result[len(result)-1] == '\n' {
708 result = result[:len(result)-1]
709 }
710
711 return result, nil
712 }
713