groupware: add suppliers support for object changes

This commit is contained in:
Pascal Bleser
2026-06-25 16:58:07 +02:00
parent 943d5dc462
commit e1bd8e285f
9 changed files with 339 additions and 49 deletions

View File

@@ -397,3 +397,12 @@ func Flatten[T any](s [][]T) []T {
}
return result
}
func AllMatch[T any](s []T, predicate func(e T) bool) bool {
for _, e := range s {
if !predicate(e) {
return false
}
}
return true
}

View File

@@ -19,7 +19,7 @@ func (g *Groupware) GetAddressbookById(w http.ResponseWriter, r *http.Request) {
// Get the changes to Address Books since a certain State.
// @api:tags addressbook,changes
func (g *Groupware) GetAddressBookChanges(w http.ResponseWriter, r *http.Request) {
changes(AddressBook, w, r, g, g.jmap.GetAddressbookChanges)
changes(AddressBook, w, r, g, g.addressbooksChanges)
}
func (g *Groupware) CreateAddressBook(w http.ResponseWriter, r *http.Request) {
@@ -39,3 +39,11 @@ func (g *Groupware) addressbooks(accountId jmap.AccountId, ids []string, ctx jma
return jmap.AddressBookGetResponse{AccountId: accountId, State: state, NotFound: notFound, List: list}
})
}
func (g *Groupware) addressbooksChanges(accountId jmap.AccountId, sinceState jmap.State, maxChanges uint, ctx jmap.Context) (jmap.Result[jmap.AddressBookChanges], error) {
return schanges(g.addressBookChangesSuppliers, accountId, sinceState, maxChanges, ctx,
func(accountId jmap.AccountId, oldState, newState jmap.State, created, updated []jmap.AddressBook, destroyed []string, hasMoreChanges bool) jmap.AddressBookChanges {
return jmap.AddressBookChanges{HasMoreChanges: hasMoreChanges, OldState: oldState, NewState: newState, Created: created, Updated: updated, Destroyed: destroyed}
},
)
}

View File

@@ -67,7 +67,7 @@ func (g *Groupware) GetAllContacts(w http.ResponseWriter, r *http.Request) {
// Get changes to Contacts since a given State
// @api:tags contact,changes
func (g *Groupware) GetContactsChanges(w http.ResponseWriter, r *http.Request) {
changes(Contact, w, r, g, g.jmap.GetContactCardChanges)
changes(Contact, w, r, g, g.contactsChanges)
}
func (g *Groupware) CreateContact(w http.ResponseWriter, r *http.Request) {
@@ -83,9 +83,19 @@ func (g *Groupware) ModifyContact(w http.ResponseWriter, r *http.Request) {
}
func (g *Groupware) contacts(accountId jmap.AccountId, ids []string, ctx jmap.Context) (jmap.Result[jmap.ContactCardGetResponse], error) {
return slist(g.contactCardListSuppliers, accountId, ids, ctx, func(accountId jmap.AccountId, state jmap.State, notFound []string, list []jmap.ContactCard) jmap.ContactCardGetResponse {
return jmap.ContactCardGetResponse{AccountId: accountId, State: state, NotFound: notFound, List: list}
})
return slist(g.contactCardListSuppliers, accountId, ids, ctx,
func(accountId jmap.AccountId, state jmap.State, notFound []string, list []jmap.ContactCard) jmap.ContactCardGetResponse {
return jmap.ContactCardGetResponse{AccountId: accountId, State: state, NotFound: notFound, List: list}
},
)
}
func (g *Groupware) contactsChanges(accountId jmap.AccountId, sinceState jmap.State, maxChanges uint, ctx jmap.Context) (jmap.Result[jmap.ContactCardChanges], error) {
return schanges(g.contactCardChangesSuppliers, accountId, sinceState, maxChanges, ctx,
func(accountId jmap.AccountId, oldState, newState jmap.State, created, updated []jmap.ContactCard, destroyed []string, hasMoreChanges bool) jmap.ContactCardChanges {
return jmap.ContactCardChanges{HasMoreChanges: hasMoreChanges, OldState: oldState, NewState: newState, Created: created, Updated: updated, Destroyed: destroyed}
},
)
}
func (g *Groupware) queryContacts(accountIds []jmap.AccountId, qps QueryParamsSupplier, limit *uint, //NOSONAR

View File

@@ -117,9 +117,11 @@ type Groupware struct {
// A threadsafe counter to generate the job IDs.
jobCounter atomic.Uint64
addressBookListSuppliers []ListSupplier[jmap.AddressBook, jmap.AddressBookGetResponse]
contactCardQuerySuppliers []QuerySupplier[jmap.ContactCard, *jmap.ContactCardSearchResults, jmap.ContactCardFilterElement, jmap.ContactCardComparator]
contactCardListSuppliers []ListSupplier[jmap.ContactCard, jmap.ContactCardGetResponse]
addressBookListSuppliers []ListSupplier[jmap.AddressBook, jmap.AddressBookGetResponse]
addressBookChangesSuppliers []ChangesSupplier[jmap.AddressBook, jmap.AddressBookChanges]
contactCardQuerySuppliers []QuerySupplier[jmap.ContactCard, *jmap.ContactCardSearchResults, jmap.ContactCardFilterElement, jmap.ContactCardComparator]
contactCardListSuppliers []ListSupplier[jmap.ContactCard, jmap.ContactCardGetResponse]
contactCardChangesSuppliers []ChangesSupplier[jmap.ContactCard, jmap.ContactCardChanges]
}
// An error during the Groupware initialization.
@@ -393,27 +395,33 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
}
addressBookListSuppliers := []ListSupplier[jmap.AddressBook, jmap.AddressBookGetResponse]{}
addressBookChangesSuppliers := []ChangesSupplier[jmap.AddressBook, jmap.AddressBookChanges]{}
contactCardQuerySuppliers := []QuerySupplier[jmap.ContactCard, *jmap.ContactCardSearchResults, jmap.ContactCardFilterElement, jmap.ContactCardComparator]{}
contactCardListSuppliers := []ListSupplier[jmap.ContactCard, jmap.ContactCardGetResponse]{}
contactCardChangesSuppliers := []ChangesSupplier[jmap.ContactCard, jmap.ContactCardChanges]{}
{
{
j := newJmapAddressBookSupplier(&jmapClient)
addressBookListSuppliers = append(addressBookListSuppliers, j)
addressBookChangesSuppliers = append(addressBookChangesSuppliers, j)
}
{
j := newJmapContactCardSupplier(&jmapClient)
contactCardQuerySuppliers = append(contactCardQuerySuppliers, j)
contactCardListSuppliers = append(contactCardListSuppliers, j)
contactCardChangesSuppliers = append(contactCardChangesSuppliers, j)
}
if config.EnableMockData {
{
m := newMockAddressBookSupplier()
addressBookListSuppliers = append(addressBookListSuppliers, m)
addressBookChangesSuppliers = append(addressBookChangesSuppliers, m)
}
{
m := newMockContactCardSupplier()
contactCardQuerySuppliers = append(contactCardQuerySuppliers, m)
contactCardListSuppliers = append(contactCardListSuppliers, m)
contactCardChangesSuppliers = append(contactCardChangesSuppliers, m)
}
}
}
@@ -446,12 +454,14 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
publicUrl: publicUrl,
sendDurationsResponse: sendDurationsResponse,
},
eventChannel: eventChannel,
jobsChannel: jobsChannel,
jobCounter: atomic.Uint64{},
addressBookListSuppliers: addressBookListSuppliers,
contactCardQuerySuppliers: contactCardQuerySuppliers,
contactCardListSuppliers: contactCardListSuppliers,
eventChannel: eventChannel,
jobsChannel: jobsChannel,
jobCounter: atomic.Uint64{},
addressBookListSuppliers: addressBookListSuppliers,
addressBookChangesSuppliers: addressBookChangesSuppliers,
contactCardQuerySuppliers: contactCardQuerySuppliers,
contactCardListSuppliers: contactCardListSuppliers,
contactCardChangesSuppliers: contactCardChangesSuppliers,
}
for w := 1; w <= workerPoolSize; w++ {

View File

@@ -11,6 +11,7 @@ type JmapAddressBookSupplier struct {
}
var _ ListSupplier[jmap.AddressBook, jmap.AddressBookGetResponse] = &JmapAddressBookSupplier{}
var _ ChangesSupplier[jmap.ContactCard, jmap.ContactCardChanges] = &JmapContactCardSupplier{}
func newJmapAddressBookSupplier(client *jmap.Client) *JmapAddressBookSupplier {
return &JmapAddressBookSupplier{client: client}
@@ -21,9 +22,15 @@ const jmapAddressBookSupplierId = SupplierId("jmap")
func (c *JmapAddressBookSupplier) GetId() SupplierId {
return jmapAddressBookSupplierId
}
func (c *JmapAddressBookSupplier) IsMine(id string) bool {
return id != "" && !strings.Contains(id, ":")
}
func (c *JmapAddressBookSupplier) GetAll(accountId jmap.AccountId, ids []string, ctx jmap.Context) (jmap.Result[jmap.AddressBookGetResponse], error) {
return c.client.GetAddressbooks(accountId, ids, ctx)
}
func (c *JmapAddressBookSupplier) GetChanges(accountId jmap.AccountId, sinceState jmap.State, maxChanges uint, ctx jmap.Context) (jmap.Result[jmap.AddressBookChanges], error) {
return c.client.GetAddressbookChanges(accountId, sinceState, maxChanges, ctx)
}

View File

@@ -9,11 +9,12 @@ import (
)
type MockAddressBookSupplier struct {
addressBook jmap.AddressBook
addressBooks []jmap.AddressBook
state jmap.State
}
var MockAddressBookSupplierInstance *MockAddressBookSupplier = &MockAddressBookSupplier{
addressBook: jmap.AddressBook{
addressBooks: []jmap.AddressBook{{
Id: "mock:1",
Name: "Automatic Addressbook",
Description: "Users",
@@ -25,7 +26,8 @@ var MockAddressBookSupplierInstance *MockAddressBookSupplier = &MockAddressBookS
MayAdmin: false,
MayDelete: false,
},
},
}},
state: jmap.State("1"),
}
var _ ListSupplier[jmap.AddressBook, jmap.AddressBookGetResponse] = &MockAddressBookSupplier{}
@@ -41,7 +43,7 @@ func (c *MockAddressBookSupplier) IsMine(id string) bool {
return strings.HasPrefix(id, "mock:")
}
func (c *MockAddressBookSupplier) GetAll(accountId jmap.AccountId, ids []string, ctx jmap.Context) (jmap.Result[jmap.AddressBookGetResponse], error) {
abooks := []jmap.AddressBook{c.addressBook}
abooks := c.addressBooks
if len(ids) > 0 {
abooks = structs.Filter(abooks, func(a jmap.AddressBook) bool { return slices.Contains(ids, a.Id) })
}
@@ -57,3 +59,22 @@ func (c *MockAddressBookSupplier) GetAll(accountId jmap.AccountId, ids []string,
nil,
), nil
}
func (c *MockAddressBookSupplier) GetChanges(accountId jmap.AccountId, sinceState jmap.State, maxChanges uint, ctx jmap.Context) (jmap.Result[jmap.AddressBookChanges], error) {
if sinceState == c.state {
ch := jmap.AddressBookChanges{
HasMoreChanges: false,
OldState: c.state,
NewState: c.state,
}
return jmap.NewResult(ch, jmap.EmptySessionState, c.state, jmap.NoLanguage, nil), nil
} else {
// what happens when the state is unknown?
ch := jmap.AddressBookChanges{
HasMoreChanges: false,
NewState: c.state,
Created: c.addressBooks,
}
return jmap.NewResult(ch, jmap.EmptySessionState, c.state, jmap.NoLanguage, nil), nil
}
}

View File

@@ -12,6 +12,7 @@ type JmapContactCardSupplier struct {
var _ QuerySupplier[jmap.ContactCard, *jmap.ContactCardSearchResults, jmap.ContactCardFilterElement, jmap.ContactCardComparator] = &JmapContactCardSupplier{}
var _ ListSupplier[jmap.ContactCard, jmap.ContactCardGetResponse] = &JmapContactCardSupplier{}
var _ ChangesSupplier[jmap.ContactCard, jmap.ContactCardChanges] = &JmapContactCardSupplier{}
func newJmapContactCardSupplier(client *jmap.Client) *JmapContactCardSupplier {
return &JmapContactCardSupplier{client: client}
@@ -22,9 +23,11 @@ const jmapContactCardSupplierId = SupplierId("jmap")
func (c *JmapContactCardSupplier) GetId() SupplierId {
return jmapContactCardSupplierId
}
func (c *JmapContactCardSupplier) IsMine(id string) bool {
return id != "" && !strings.Contains(id, ":")
}
func (c *JmapContactCardSupplier) Query(accountIds []jmap.AccountId, qps QueryParamsSupplier, limit *uint, filter jmap.ContactCardFilterElement, sortBy []jmap.ContactCardComparator, calculateTotal bool, ctx jmap.Context) (jmap.Result[map[jmap.AccountId]*jmap.ContactCardSearchResults], error) { //NOSONAR
if m, err := mapQueryParams(c.GetId(), accountIds, qps); err != nil {
return jmap.ZeroResultV[map[jmap.AccountId]*jmap.ContactCardSearchResults](), err
@@ -32,6 +35,11 @@ func (c *JmapContactCardSupplier) Query(accountIds []jmap.AccountId, qps QueryPa
return c.client.QueryContactCards(m, limit, filter, sortBy, calculateTotal, ctx)
}
}
func (c *JmapContactCardSupplier) GetAll(accountId jmap.AccountId, ids []string, ctx jmap.Context) (jmap.Result[jmap.ContactCardGetResponse], error) {
return c.client.GetContactCards(accountId, ids, ctx)
}
func (c *JmapContactCardSupplier) GetChanges(accountId jmap.AccountId, sinceState jmap.State, maxChanges uint, ctx jmap.Context) (jmap.Result[jmap.ContactCardChanges], error) {
return c.client.GetContactCardChanges(accountId, sinceState, maxChanges, ctx)
}

View File

@@ -11,6 +11,7 @@ import (
type MockContactCardSupplier struct {
contacts []jmap.ContactCard
state jmap.State
}
var MockContactCardSupplierInstance *MockContactCardSupplier = &MockContactCardSupplier{
@@ -41,10 +42,12 @@ var MockContactCardSupplierInstance *MockContactCardSupplier = &MockContactCardS
},
},
},
state: jmap.State("1"),
}
var _ QuerySupplier[jmap.ContactCard, *jmap.ContactCardSearchResults, jmap.ContactCardFilterElement, jmap.ContactCardComparator] = &MockContactCardSupplier{}
var _ ListSupplier[jmap.ContactCard, jmap.ContactCardGetResponse] = &MockContactCardSupplier{}
var _ ChangesSupplier[jmap.ContactCard, jmap.ContactCardChanges] = &MockContactCardSupplier{}
func newMockContactCardSupplier() *MockContactCardSupplier {
return MockContactCardSupplierInstance
@@ -128,5 +131,24 @@ func (c *MockContactCardSupplier) Query(accountIds []jmap.AccountId, qps QueryPa
payload[accountId] = res
}
return jmap.NewResult(payload, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, nil), nil
return jmap.NewResult(payload, jmap.EmptySessionState, c.state, jmap.NoLanguage, nil), nil
}
func (c *MockContactCardSupplier) GetChanges(accountId jmap.AccountId, sinceState jmap.State, maxChanges uint, ctx jmap.Context) (jmap.Result[jmap.ContactCardChanges], error) {
if sinceState == c.state {
ch := jmap.ContactCardChanges{
HasMoreChanges: false,
OldState: c.state,
NewState: c.state,
}
return jmap.NewResult(ch, jmap.EmptySessionState, c.state, jmap.NoLanguage, nil), nil
} else {
// what happens when the state is unknown?
ch := jmap.ContactCardChanges{
HasMoreChanges: false,
NewState: c.state,
Created: c.contacts,
}
return jmap.NewResult(ch, jmap.EmptySessionState, c.state, jmap.NoLanguage, nil), nil
}
}

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"slices"
"strings"
"time"
"github.com/opencloud-eu/opencloud/pkg/jmap"
@@ -15,6 +16,8 @@ type SupplierId string
const EmptySupplierId = ""
const DefaultSupplierId = SupplierId("jmap")
type Supplier[T jmap.Foo] interface {
GetId() SupplierId
IsMine(id string) bool
@@ -30,6 +33,11 @@ type QuerySupplier[T jmap.Foo, R jmap.SearchResults[T], F jmap.FilterElement[T],
Supplier[T]
}
type ChangesSupplier[T jmap.Foo, C jmap.Changes[T]] interface {
GetChanges(accountId jmap.AccountId, sinceState jmap.State, maxChanges uint, ctx jmap.Context) (jmap.Result[C], error)
Supplier[T]
}
// queryFunc func(req Request, accountIds []string, qps QueryParamsSupplier, limit *uint, filter FILTER, sortBy []COMP, ctx jmap.Context) (jmap.Result[SEARCHRESULTS], NextToken, error),
func curryQueryFunc[SRES jmap.SearchResults[T], T jmap.Foo, FILTER any, COMP any](
f func(accountIds []jmap.AccountId, qps QueryParamsSupplier, limit *uint, filter FILTER, sortBy []COMP, calculateTotal bool, ctx jmap.Context) (jmap.Result[SRES], NextToken, error),
@@ -89,6 +97,86 @@ func agg[T jmap.Idable, R jmap.GetResponse[T]](accountId jmap.AccountId, supplie
return ctor(accountId, state, notFounds, lists), nil
}
func aggChanges[T jmap.Idable, R jmap.Changes[T]](accountId jmap.AccountId, supplierIds []SupplierId, responses []*R, //NOSONAR
ctor func(accountId jmap.AccountId, oldState jmap.State, newState jmap.State, created []T, updated []T, destroyed []string, hasMoreChanges bool) R,
) (R, error) {
if len(responses) < 1 {
var zero R
return zero, fmt.Errorf("requires at least one response")
}
created := structs.Concat(structs.Map(responses, func(e *R) []T {
if e != nil {
return (*e).GetCreated()
} else {
return []T{}
}
})...)
updated := structs.Concat(structs.Map(responses, func(e *R) []T {
if e != nil {
return (*e).GetUpdated()
} else {
return []T{}
}
})...)
destroyed := structs.Concat(structs.Map(responses, func(e *R) []string {
if e != nil {
return (*e).GetDestroyed()
} else {
return []string{}
}
})...)
oldStates, err := structs.MeshMap(supplierIds, responses, func(id SupplierId, e *R) (SupplierId, jmap.State, bool) {
if e != nil {
state := (*e).GetOldState()
if state != jmap.EmptyState {
return id, state, true
} else {
return id, jmap.EmptyState, false
}
} else {
return "", jmap.EmptyState, false
}
})
if err != nil {
var zero R
return zero, err
}
oldState, err := combineState(oldStates)
if err != nil {
var zero R
return zero, err
}
newStates, err := structs.MeshMap(supplierIds, responses, func(id SupplierId, e *R) (SupplierId, jmap.State, bool) {
if e != nil {
state := (*e).GetNewState()
if state != jmap.EmptyState {
return id, state, true
} else {
return id, jmap.EmptyState, false
}
} else {
return "", jmap.EmptyState, false
}
})
if err != nil {
var zero R
return zero, err
}
newState, err := combineState(newStates)
if err != nil {
var zero R
return zero, err
}
hasMoreChanges := structs.AllMatch(structs.Map(responses, func(e *R) bool {
if e != nil {
return (*e).GetHasMoreChanges()
} else {
return false
}
}), func(b bool) bool { return b })
return ctor(accountId, oldState, newState, created, updated, destroyed, hasMoreChanges), nil
}
func slist[T jmap.Idable, G jmap.GetResponse[T], S ListSupplier[T, G]](suppliers []S, accountId jmap.AccountId, ids []string, ctx jmap.Context, //NOSONAR
ctor func(accountId jmap.AccountId, state jmap.State, notFound []string, list []T) G) (jmap.Result[G], error) {
switch len(suppliers) {
@@ -117,34 +205,143 @@ func slist[T jmap.Idable, G jmap.GetResponse[T], S ListSupplier[T, G]](suppliers
}
}
return jmap.RefineResultSlice(results, func(payloads []*G, sessionStates []*jmap.SessionState, states []*jmap.State, langs []*jmap.Language) (G, jmap.SessionState, jmap.State, jmap.Language, error) {
resp, err := agg(accountId, supplierIds, payloads, ctor)
if err != nil {
return resp, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, err
}
m, err := structs.MeshMap(supplierIds, sessionStates, func(id SupplierId, state *jmap.SessionState) (SupplierId, jmap.SessionState, bool) {
if state != nil && *state != jmap.EmptySessionState {
return id, *state, true
} else {
return id, jmap.EmptySessionState, false
}
})
if err != nil {
return resp, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, err
}
sessionState, err := combineState(m)
if err != nil {
return resp, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, err
}
lang := jmap.NoLanguage
if f, ok := structs.First(langs, func(l *jmap.Language) bool { return l != nil && *l != jmap.NoLanguage }); ok {
lang = *f
}
return resp, sessionState, resp.GetState(), lang, nil
return smeld[T](supplierIds, results, func(payloads []*G) (G, error) {
return agg(accountId, supplierIds, payloads, ctor)
}, func(resp G) jmap.State {
return resp.GetState()
})
/*
return jmap.RefineResultSlice(results, func(payloads []*G, sessionStates []*jmap.SessionState, states []*jmap.State, langs []*jmap.Language) (G, jmap.SessionState, jmap.State, jmap.Language, error) {
resp, err := agg(accountId, supplierIds, payloads, ctor)
if err != nil {
return resp, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, err
}
m, err := structs.MeshMap(supplierIds, sessionStates, func(id SupplierId, state *jmap.SessionState) (SupplierId, jmap.SessionState, bool) {
if state != nil && *state != jmap.EmptySessionState {
return id, *state, true
} else {
return id, jmap.EmptySessionState, false
}
})
if err != nil {
return resp, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, err
}
sessionState, err := combineState(m)
if err != nil {
return resp, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, err
}
lang := jmap.NoLanguage
if f, ok := structs.First(langs, func(l *jmap.Language) bool { return l != nil && *l != jmap.NoLanguage }); ok {
lang = *f
}
return resp, sessionState, resp.GetState(), lang, nil
})
*/
}
}
func schanges[T jmap.Idable, C jmap.Changes[T], S ChangesSupplier[T, C]](suppliers []S, accountId jmap.AccountId, sinceState jmap.State, maxChanges uint, ctx jmap.Context, //NOSONAR
ctor func(accountId jmap.AccountId, oldState jmap.State, newState jmap.State, created []T, updated []T, destroyed []string, hasMoreChanges bool) C,
) (jmap.Result[C], error) {
switch len(suppliers) {
case 0:
return jmap.ZeroResultV[C](), nil
case 1:
return suppliers[0].GetChanges(accountId, sinceState, maxChanges, ctx)
default:
results := make([]*jmap.Result[C], len(suppliers))
supplierIds := make([]SupplierId, len(suppliers))
stateBySupplierId, err := splitState(sinceState, DefaultSupplierId)
if err != nil {
return jmap.ZeroResultV[C](), err
}
for i, supplier := range suppliers {
supplierIds[i] = supplier.GetId()
state := jmap.EmptyState
if v, ok := stateBySupplierId[supplier.GetId()]; ok {
state = v
}
// we are not injecting id prefixes here for all the objects, as each supplier is responsible for doing that if necessary
if result, err := supplier.GetChanges(accountId, state, maxChanges, ctx); err != nil {
return result, err
} else {
results[i] = &result
}
}
// TODO need to reduce the maxChanges? this is quite complex to implement, if even possible, not doing that for now
return smeld[T](supplierIds, results, func(payloads []*C) (C, error) {
return aggChanges(accountId, supplierIds, payloads, ctor)
}, func(resp C) jmap.State {
return resp.GetNewState()
})
/*
return jmap.RefineResultSlice(results, func(payloads []*C, sessionStates []*jmap.SessionState, states []*jmap.State, langs []*jmap.Language) (C, jmap.SessionState, jmap.State, jmap.Language, error) {
resp, err := aggChanges(accountId, supplierIds, payloads, ctor)
if err != nil {
return resp, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, err
}
m, err := structs.MeshMap(supplierIds, sessionStates, func(id SupplierId, state *jmap.SessionState) (SupplierId, jmap.SessionState, bool) {
if state != nil && *state != jmap.EmptySessionState {
return id, *state, true
} else {
return id, jmap.EmptySessionState, false
}
})
if err != nil {
return resp, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, err
}
sessionState, err := combineState(m)
if err != nil {
return resp, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, err
}
lang := jmap.NoLanguage
if f, ok := structs.First(langs, func(l *jmap.Language) bool { return l != nil && *l != jmap.NoLanguage }); ok {
lang = *f
}
return resp, sessionState, resp.GetNewState(), lang, nil
})
*/
}
}
func smeld[T jmap.Idable, C any](
supplierIds []SupplierId,
results []*jmap.Result[C],
aggregator func(payloads []*C) (C, error),
stateSupplier func(resp C) jmap.State,
) (jmap.Result[C], error) {
return jmap.RefineResultSlice(results, func(payloads []*C, sessionStates []*jmap.SessionState, states []*jmap.State, langs []*jmap.Language) (C, jmap.SessionState, jmap.State, jmap.Language, error) {
resp, err := aggregator(payloads)
if err != nil {
return resp, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, err
}
m, err := structs.MeshMap(supplierIds, sessionStates, func(id SupplierId, state *jmap.SessionState) (SupplierId, jmap.SessionState, bool) {
if state != nil && *state != jmap.EmptySessionState {
return id, *state, true
} else {
return id, jmap.EmptySessionState, false
}
})
if err != nil {
return resp, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, err
}
sessionState, err := combineState(m)
if err != nil {
return resp, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, err
}
lang := jmap.NoLanguage
if f, ok := structs.First(langs, func(l *jmap.Language) bool { return l != nil && *l != jmap.NoLanguage }); ok {
lang = *f
}
return resp, sessionState, stateSupplier(resp), lang, nil
})
}
func fillMissingAccounts(qps QueryParamsSupplier, supplierId SupplierId, accountIds []jmap.AccountId, n map[jmap.AccountId]jmap.QueryParams) error {
for _, accountId := range accountIds {
if _, ok := n[accountId]; !ok {
@@ -403,7 +600,7 @@ func squery[T jmap.Idable, R jmap.SearchResults[T], S QuerySupplier[T, R, F, C],
const combinedStateEncodingPrefix = "="
func combineState[K ~string, S jmap.State | jmap.SessionState](m map[K]S) (S, error) {
func combineState[S jmap.State | jmap.SessionState](m map[SupplierId]S) (S, error) {
if b, err := json.Marshal(m); err != nil {
return "", err
} else {
@@ -412,14 +609,13 @@ func combineState[K ~string, S jmap.State | jmap.SessionState](m map[K]S) (S, er
}
}
/*
func splitState[K ~string, S jmap.State | jmap.SessionState](state S) (map[K]S, error) {
func splitState[S jmap.State | jmap.SessionState](state S, defaultSupplierId SupplierId) (map[SupplierId]S, error) {
s := string(state)
if strings.HasPrefix(s, combinedStateEncodingPrefix) {
if b, err := base64.RawURLEncoding.DecodeString(s[len(combinedStateEncodingPrefix):]); err != nil {
return nil, err
} else {
m := map[K]S{}
m := map[SupplierId]S{}
if err := json.Unmarshal(b, &m); err != nil {
return nil, err
} else {
@@ -427,7 +623,6 @@ func splitState[K ~string, S jmap.State | jmap.SessionState](state S) (map[K]S,
}
}
} else {
return map[K]S{K("jmap"): state}, nil
return map[SupplierId]S{defaultSupplierId: state}, nil
}
}
*/