package bus import ( "bytes" "crypto/rand" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "strings" "time" "github.com/nats-io/nats.go" "git.novox.be/novox/mesh-sdk/go/buscalls" ) // FileField is the member of a chunked verb's answer that carries the file received: its whole bytes, base64 (design 47 G // A-249). const FileField = "file" // AskAsChunked is AskAs of a seat's verb whose definition says `chunks` (design 47 G ยง681, A-237, A-249): it chooses the call // token (the body's `call`), hears the chunk subjects of that call before it asks, receives the chunks the verb's answer // counts with buscalls.SeatChunks, each acknowledged before the next, and answers the verb's own answer with the whole // file added as FileField. The asking bundle receives no chunk itself. func (c *Conn) AskAsChunked(module, key string, body any) (Answered, error) { seat, verb, ok := strings.Cut(strings.TrimPrefix(key, "seat:"), ".") if !strings.HasPrefix(key, "seat:") || !ok || strings.Contains(verb, "@") { return Answered{}, fmt.Errorf("%q is no seat:. of no one machine, which a chunked answer needs", key) } args := map[string]any{} if raw, err := json.Marshal(body); err != nil { return Answered{}, err } else if string(raw) != "null" { if err := json.Unmarshal(raw, &args); err != nil { return Answered{}, fmt.Errorf("the arguments of %s are not an object: %w", key, err) } } var bits [16]byte if _, err := rand.Read(bits[:]); err != nil { return Answered{}, err } send := buscalls.SeatChunks{Seat: seat, Verb: verb, Call: hex.EncodeToString(bits[:])} args["call"] = send.Call chunks := make(chan *nats.Msg, 4) sub, err := c.nc.ChanSubscribe(send.Chunks(), chunks) if err != nil { return Answered{}, fmt.Errorf("the chunks of %s cannot be heard: %w", key, err) } defer func() { _ = sub.Unsubscribe() }() answered, err := c.AskAs(module, key, args) if err != nil { return answered, err } fields := map[string]json.RawMessage{} if err := json.Unmarshal(answered.Result, &fields); err != nil { return answered, fmt.Errorf("%s answered what is no object: %w", key, err) } var count int64 if raw, has := fields["chunks"]; has { if err := json.Unmarshal(raw, &count); err != nil { return answered, fmt.Errorf("%s answered chunks that is no count: %w", key, err) } } var file bytes.Buffer if count > 0 { next := func() (string, []byte, error) { select { case m := <-chunks: return m.Subject, m.Data, nil case <-time.After(buscalls.SeatAckWithin): return "", nil, buscalls.ErrSeatChunkMissed } } ack := func(subject string) error { _, err := c.nc.Request(subject, []byte("{}"), buscalls.SeatAckWithin) return err } if _, err := send.Receive(count, next, ack, &file); err != nil { return answered, fmt.Errorf("the chunks of %s: %w", key, err) } } if _, taken := fields[FileField]; taken { return answered, errors.New(key + " answers a member named " + FileField + " itself") } enc, _ := json.Marshal(base64.StdEncoding.EncodeToString(file.Bytes())) fields[FileField] = enc out, err := json.Marshal(fields) if err != nil { return answered, err } answered.Result = out return answered, nil }