mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-07-13 17:12:05 -04:00
groupware: add support for multi-supplier getting a Contact by ID
This commit is contained in:
@@ -6,32 +6,24 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/jmap"
|
||||
)
|
||||
|
||||
type JmapContactCardSupplier struct {
|
||||
type JmapAddressBookSupplier struct {
|
||||
client *jmap.Client
|
||||
}
|
||||
|
||||
var _ ListSupplier[jmap.AddressBook, jmap.AddressBookGetResponse] = &JmapContactCardSupplier{}
|
||||
var _ QuerySupplier[jmap.ContactCard, *jmap.ContactCardSearchResults, jmap.ContactCardFilterElement, jmap.ContactCardComparator] = &JmapContactCardSupplier{}
|
||||
var _ ListSupplier[jmap.AddressBook, jmap.AddressBookGetResponse] = &JmapAddressBookSupplier{}
|
||||
|
||||
func newJmapContactCardSupplier(client *jmap.Client) *JmapContactCardSupplier {
|
||||
return &JmapContactCardSupplier{client: client}
|
||||
func newJmapAddressBookSupplier(client *jmap.Client) *JmapAddressBookSupplier {
|
||||
return &JmapAddressBookSupplier{client: client}
|
||||
}
|
||||
|
||||
const jmapContactCardSupplierid = SupplierId("jmap")
|
||||
const jmapAddressBookSupplierId = SupplierId("jmap")
|
||||
|
||||
func (c *JmapContactCardSupplier) GetId() SupplierId {
|
||||
return jmapContactCardSupplierid
|
||||
func (c *JmapAddressBookSupplier) GetId() SupplierId {
|
||||
return jmapAddressBookSupplierId
|
||||
}
|
||||
func (c *JmapContactCardSupplier) IsMine(id string) bool {
|
||||
func (c *JmapAddressBookSupplier) IsMine(id string) bool {
|
||||
return id != "" && !strings.Contains(id, ":")
|
||||
}
|
||||
func (c *JmapContactCardSupplier) GetAll(accountId jmap.AccountId, ids []string, ctx jmap.Context) (jmap.Result[jmap.AddressBookGetResponse], error) {
|
||||
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 *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
|
||||
} else {
|
||||
return c.client.QueryContactCards(m, limit, filter, sortBy, calculateTotal, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,16 +5,14 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/jmap"
|
||||
"github.com/opencloud-eu/opencloud/pkg/jscontact"
|
||||
"github.com/opencloud-eu/opencloud/pkg/structs"
|
||||
)
|
||||
|
||||
type MockContactCardSupplier struct {
|
||||
type MockAddressBookSupplier struct {
|
||||
addressBook jmap.AddressBook
|
||||
contacts []jmap.ContactCard
|
||||
}
|
||||
|
||||
var MockContactCardSupplierInstance *MockContactCardSupplier = &MockContactCardSupplier{
|
||||
var MockAddressBookSupplierInstance *MockAddressBookSupplier = &MockAddressBookSupplier{
|
||||
addressBook: jmap.AddressBook{
|
||||
Id: "mock:1",
|
||||
Name: "Automatic Addressbook",
|
||||
@@ -28,49 +26,21 @@ var MockContactCardSupplierInstance *MockContactCardSupplier = &MockContactCardS
|
||||
MayDelete: false,
|
||||
},
|
||||
},
|
||||
contacts: []jmap.ContactCard{
|
||||
{
|
||||
Id: "mock:alan",
|
||||
AddressBookIds: map[string]bool{"mock:1": true},
|
||||
Type: jscontact.ContactCardType,
|
||||
Version: jmap.DEFAULT_CONTACT_CARD_VERSION,
|
||||
Created: mustParseTime("2026-05-26T10:21:00.000Z"),
|
||||
Kind: jscontact.ContactCardKindIndividual,
|
||||
ProdId: "OC:mock",
|
||||
Uid: "dc2858d2-4826-412d-afc9-c4492f8f84bc",
|
||||
Updated: mustParseTime("2026-05-26T10:21:00.000Z"),
|
||||
Name: &jscontact.Name{
|
||||
Type: jscontact.NameType,
|
||||
Components: []jscontact.NameComponent{
|
||||
{Kind: jscontact.NameComponentKindGiven, Value: "Alan"},
|
||||
{Kind: jscontact.NameComponentKindSurname, Value: "Turing"},
|
||||
},
|
||||
DefaultSeparator: " ",
|
||||
IsOrdered: true,
|
||||
},
|
||||
Emails: map[string]jscontact.EmailAddress{
|
||||
"eedujae1": {
|
||||
Address: "alan@example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var _ ListSupplier[jmap.AddressBook, jmap.AddressBookGetResponse] = &MockContactCardSupplier{}
|
||||
var _ QuerySupplier[jmap.ContactCard, *jmap.ContactCardSearchResults, jmap.ContactCardFilterElement, jmap.ContactCardComparator] = &MockContactCardSupplier{}
|
||||
var _ ListSupplier[jmap.AddressBook, jmap.AddressBookGetResponse] = &MockAddressBookSupplier{}
|
||||
|
||||
func newMockContactCardSupplier() *MockContactCardSupplier {
|
||||
return MockContactCardSupplierInstance
|
||||
func newMockAddressBookSupplier() *MockAddressBookSupplier {
|
||||
return MockAddressBookSupplierInstance
|
||||
}
|
||||
|
||||
func (c *MockContactCardSupplier) GetId() SupplierId {
|
||||
func (c *MockAddressBookSupplier) GetId() SupplierId {
|
||||
return SupplierId("mock")
|
||||
}
|
||||
func (c *MockContactCardSupplier) IsMine(id string) bool {
|
||||
func (c *MockAddressBookSupplier) IsMine(id string) bool {
|
||||
return strings.HasPrefix(id, "mock:")
|
||||
}
|
||||
func (c *MockContactCardSupplier) GetAll(accountId jmap.AccountId, ids []string, ctx jmap.Context) (jmap.Result[jmap.AddressBookGetResponse], error) {
|
||||
func (c *MockAddressBookSupplier) GetAll(accountId jmap.AccountId, ids []string, ctx jmap.Context) (jmap.Result[jmap.AddressBookGetResponse], error) {
|
||||
abooks := []jmap.AddressBook{c.addressBook}
|
||||
if len(ids) > 0 {
|
||||
abooks = structs.Filter(abooks, func(a jmap.AddressBook) bool { return slices.Contains(ids, a.Id) })
|
||||
@@ -87,60 +57,3 @@ func (c *MockContactCardSupplier) GetAll(accountId jmap.AccountId, ids []string,
|
||||
nil,
|
||||
), nil
|
||||
}
|
||||
func (c *MockContactCardSupplier) 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
|
||||
payload := make(map[jmap.AccountId]*jmap.ContactCardSearchResults, len(accountIds))
|
||||
total := len(c.contacts)
|
||||
for _, accountId := range accountIds {
|
||||
all := []jmap.ContactCard{}
|
||||
var qp jmap.QueryParams
|
||||
if q, ok, err := qps.ForSupplier(c.GetId(), accountId); err != nil {
|
||||
return jmap.ZeroResultV[map[jmap.AccountId]*jmap.ContactCardSearchResults](), err
|
||||
} else if ok {
|
||||
qp = q
|
||||
} else {
|
||||
qp = jmap.NullQueryParams
|
||||
}
|
||||
|
||||
p := uint(qp.Position)
|
||||
if qp.Position < total {
|
||||
all = c.contacts[qp.Position:]
|
||||
}
|
||||
if qp.Anchor != "" {
|
||||
a := slices.IndexFunc(all, func(e jmap.ContactCard) bool { return e.Id == qp.Anchor })
|
||||
if a >= 0 {
|
||||
if qp.AnchorOffset != nil {
|
||||
a += int(*qp.AnchorOffset)
|
||||
} else {
|
||||
a += 1
|
||||
}
|
||||
p += uint(a)
|
||||
if a < len(all) {
|
||||
all = all[a:]
|
||||
} else {
|
||||
all = []jmap.ContactCard{}
|
||||
}
|
||||
} else {
|
||||
all = []jmap.ContactCard{}
|
||||
}
|
||||
}
|
||||
if limit != nil {
|
||||
if len(all) > int(*limit) {
|
||||
all = all[:int(*limit)]
|
||||
}
|
||||
}
|
||||
|
||||
res := &jmap.ContactCardSearchResults{
|
||||
Results: all,
|
||||
CanCalculateChanges: false,
|
||||
Position: &p,
|
||||
Limit: limit,
|
||||
}
|
||||
if calculateTotal {
|
||||
t := uint(total)
|
||||
res.Total = &t
|
||||
}
|
||||
payload[accountId] = res
|
||||
}
|
||||
|
||||
return jmap.NewResult(payload, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, nil), nil
|
||||
}
|
||||
|
||||
@@ -46,12 +46,12 @@ func (g *Groupware) GetContactsInAddressbook(w http.ResponseWriter, r *http.Requ
|
||||
return jmap.ContactCardFilterCondition{InAddressBook: addressbookId}
|
||||
},
|
||||
[]jmap.ContactCardComparator{{Property: jmap.ContactCardPropertyCreated, IsAscending: true}},
|
||||
curryQueryFunc(g.contacts),
|
||||
curryQueryFunc(g.queryContacts),
|
||||
)
|
||||
}
|
||||
|
||||
func (g *Groupware) GetContactById(w http.ResponseWriter, r *http.Request) {
|
||||
get(Contact, w, r, g, g.jmap.GetContactCards)
|
||||
get(Contact, w, r, g, g.contacts)
|
||||
}
|
||||
|
||||
func (g *Groupware) GetAllContacts(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -60,7 +60,7 @@ func (g *Groupware) GetAllContacts(w http.ResponseWriter, r *http.Request) {
|
||||
return jmap.ContactCardFilterCondition{}
|
||||
},
|
||||
[]jmap.ContactCardComparator{{Property: jmap.ContactCardPropertyCreated, IsAscending: true}},
|
||||
curryQueryFunc(g.contacts),
|
||||
curryQueryFunc(g.queryContacts),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -82,7 +82,13 @@ func (g *Groupware) ModifyContact(w http.ResponseWriter, r *http.Request) {
|
||||
modify(Contact, w, r, g, g.jmap.UpdateContactCard)
|
||||
}
|
||||
|
||||
func (g *Groupware) contacts(accountIds []jmap.AccountId, qps QueryParamsSupplier, limit *uint, //NOSONAR
|
||||
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}
|
||||
})
|
||||
}
|
||||
|
||||
func (g *Groupware) queryContacts(accountIds []jmap.AccountId, qps QueryParamsSupplier, limit *uint, //NOSONAR
|
||||
filter jmap.ContactCardFilterElement, sortBy []jmap.ContactCardComparator, calculateTotal bool,
|
||||
ctx jmap.Context) (jmap.Result[*jmap.ContactCardSearchResults], NextToken, error) {
|
||||
return squery(g.contactCardQuerySuppliers, accountIds, qps, limit, filter, sortBy, calculateTotal, ctx,
|
||||
|
||||
37
services/groupware/pkg/groupware/contact_supplier_jmap.go
Normal file
37
services/groupware/pkg/groupware/contact_supplier_jmap.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package groupware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/jmap"
|
||||
)
|
||||
|
||||
type JmapContactCardSupplier struct {
|
||||
client *jmap.Client
|
||||
}
|
||||
|
||||
var _ QuerySupplier[jmap.ContactCard, *jmap.ContactCardSearchResults, jmap.ContactCardFilterElement, jmap.ContactCardComparator] = &JmapContactCardSupplier{}
|
||||
var _ ListSupplier[jmap.ContactCard, jmap.ContactCardGetResponse] = &JmapContactCardSupplier{}
|
||||
|
||||
func newJmapContactCardSupplier(client *jmap.Client) *JmapContactCardSupplier {
|
||||
return &JmapContactCardSupplier{client: client}
|
||||
}
|
||||
|
||||
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
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
132
services/groupware/pkg/groupware/contact_supplier_mock.go
Normal file
132
services/groupware/pkg/groupware/contact_supplier_mock.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package groupware
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/jmap"
|
||||
"github.com/opencloud-eu/opencloud/pkg/jscontact"
|
||||
"github.com/opencloud-eu/opencloud/pkg/structs"
|
||||
)
|
||||
|
||||
type MockContactCardSupplier struct {
|
||||
contacts []jmap.ContactCard
|
||||
}
|
||||
|
||||
var MockContactCardSupplierInstance *MockContactCardSupplier = &MockContactCardSupplier{
|
||||
contacts: []jmap.ContactCard{
|
||||
{
|
||||
Id: "mock:alan",
|
||||
AddressBookIds: map[string]bool{"mock:1": true},
|
||||
Type: jscontact.ContactCardType,
|
||||
Version: jmap.DEFAULT_CONTACT_CARD_VERSION,
|
||||
Created: mustParseTime("2026-05-26T10:21:00.000Z"),
|
||||
Kind: jscontact.ContactCardKindIndividual,
|
||||
ProdId: "OC:mock",
|
||||
Uid: "dc2858d2-4826-412d-afc9-c4492f8f84bc",
|
||||
Updated: mustParseTime("2026-05-26T10:21:00.000Z"),
|
||||
Name: &jscontact.Name{
|
||||
Type: jscontact.NameType,
|
||||
Components: []jscontact.NameComponent{
|
||||
{Kind: jscontact.NameComponentKindGiven, Value: "Alan"},
|
||||
{Kind: jscontact.NameComponentKindSurname, Value: "Turing"},
|
||||
},
|
||||
DefaultSeparator: " ",
|
||||
IsOrdered: true,
|
||||
},
|
||||
Emails: map[string]jscontact.EmailAddress{
|
||||
"eedujae1": {
|
||||
Address: "alan@example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var _ QuerySupplier[jmap.ContactCard, *jmap.ContactCardSearchResults, jmap.ContactCardFilterElement, jmap.ContactCardComparator] = &MockContactCardSupplier{}
|
||||
var _ ListSupplier[jmap.ContactCard, jmap.ContactCardGetResponse] = &MockContactCardSupplier{}
|
||||
|
||||
func newMockContactCardSupplier() *MockContactCardSupplier {
|
||||
return MockContactCardSupplierInstance
|
||||
}
|
||||
|
||||
func (c *MockContactCardSupplier) GetId() SupplierId {
|
||||
return SupplierId("mock")
|
||||
}
|
||||
func (c *MockContactCardSupplier) IsMine(id string) bool {
|
||||
return strings.HasPrefix(id, "mock:")
|
||||
}
|
||||
func (c *MockContactCardSupplier) GetAll(accountId jmap.AccountId, ids []string, ctx jmap.Context) (jmap.Result[jmap.ContactCardGetResponse], error) {
|
||||
contacts := c.contacts
|
||||
if len(ids) > 0 {
|
||||
contacts = structs.Filter(contacts, func(a jmap.ContactCard) bool { return slices.Contains(ids, a.Id) })
|
||||
}
|
||||
return jmap.NewResult(
|
||||
jmap.ContactCardGetResponse{
|
||||
AccountId: accountId,
|
||||
State: "mock",
|
||||
List: contacts,
|
||||
},
|
||||
jmap.EmptySessionState,
|
||||
jmap.State("mock"),
|
||||
jmap.NoLanguage,
|
||||
nil,
|
||||
), nil
|
||||
}
|
||||
func (c *MockContactCardSupplier) 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
|
||||
payload := make(map[jmap.AccountId]*jmap.ContactCardSearchResults, len(accountIds))
|
||||
total := len(c.contacts)
|
||||
for _, accountId := range accountIds {
|
||||
all := []jmap.ContactCard{}
|
||||
var qp jmap.QueryParams
|
||||
if q, ok, err := qps.ForSupplier(c.GetId(), accountId); err != nil {
|
||||
return jmap.ZeroResultV[map[jmap.AccountId]*jmap.ContactCardSearchResults](), err
|
||||
} else if ok {
|
||||
qp = q
|
||||
} else {
|
||||
qp = jmap.NullQueryParams
|
||||
}
|
||||
|
||||
p := uint(qp.Position)
|
||||
if qp.Position < total {
|
||||
all = c.contacts[qp.Position:]
|
||||
}
|
||||
if qp.Anchor != "" {
|
||||
a := slices.IndexFunc(all, func(e jmap.ContactCard) bool { return e.Id == qp.Anchor })
|
||||
if a >= 0 {
|
||||
if qp.AnchorOffset != nil {
|
||||
a += int(*qp.AnchorOffset)
|
||||
} else {
|
||||
a += 1
|
||||
}
|
||||
p += uint(a)
|
||||
if a < len(all) {
|
||||
all = all[a:]
|
||||
} else {
|
||||
all = []jmap.ContactCard{}
|
||||
}
|
||||
} else {
|
||||
all = []jmap.ContactCard{}
|
||||
}
|
||||
}
|
||||
if limit != nil {
|
||||
if len(all) > int(*limit) {
|
||||
all = all[:int(*limit)]
|
||||
}
|
||||
}
|
||||
|
||||
res := &jmap.ContactCardSearchResults{
|
||||
Results: all,
|
||||
CanCalculateChanges: false,
|
||||
Position: &p,
|
||||
Limit: limit,
|
||||
}
|
||||
if calculateTotal {
|
||||
t := uint(total)
|
||||
res.Total = &t
|
||||
}
|
||||
payload[accountId] = res
|
||||
}
|
||||
|
||||
return jmap.NewResult(payload, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, nil), nil
|
||||
}
|
||||
@@ -119,6 +119,7 @@ type Groupware struct {
|
||||
|
||||
addressBookListSuppliers []ListSupplier[jmap.AddressBook, jmap.AddressBookGetResponse]
|
||||
contactCardQuerySuppliers []QuerySupplier[jmap.ContactCard, *jmap.ContactCardSearchResults, jmap.ContactCardFilterElement, jmap.ContactCardComparator]
|
||||
contactCardListSuppliers []ListSupplier[jmap.ContactCard, jmap.ContactCardGetResponse]
|
||||
}
|
||||
|
||||
// An error during the Groupware initialization.
|
||||
@@ -393,16 +394,27 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
|
||||
|
||||
addressBookListSuppliers := []ListSupplier[jmap.AddressBook, jmap.AddressBookGetResponse]{}
|
||||
contactCardQuerySuppliers := []QuerySupplier[jmap.ContactCard, *jmap.ContactCardSearchResults, jmap.ContactCardFilterElement, jmap.ContactCardComparator]{}
|
||||
contactCardListSuppliers := []ListSupplier[jmap.ContactCard, jmap.ContactCardGetResponse]{}
|
||||
{
|
||||
{
|
||||
j := newJmapContactCardSupplier(&jmapClient)
|
||||
j := newJmapAddressBookSupplier(&jmapClient)
|
||||
addressBookListSuppliers = append(addressBookListSuppliers, j)
|
||||
}
|
||||
{
|
||||
j := newJmapContactCardSupplier(&jmapClient)
|
||||
contactCardQuerySuppliers = append(contactCardQuerySuppliers, j)
|
||||
contactCardListSuppliers = append(contactCardListSuppliers, j)
|
||||
}
|
||||
if config.EnableMockData {
|
||||
m := newMockContactCardSupplier()
|
||||
addressBookListSuppliers = append(addressBookListSuppliers, m)
|
||||
contactCardQuerySuppliers = append(contactCardQuerySuppliers, m)
|
||||
{
|
||||
m := newMockAddressBookSupplier()
|
||||
addressBookListSuppliers = append(addressBookListSuppliers, m)
|
||||
}
|
||||
{
|
||||
m := newMockContactCardSupplier()
|
||||
contactCardQuerySuppliers = append(contactCardQuerySuppliers, m)
|
||||
contactCardListSuppliers = append(contactCardListSuppliers, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,6 +451,7 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
|
||||
jobCounter: atomic.Uint64{},
|
||||
addressBookListSuppliers: addressBookListSuppliers,
|
||||
contactCardQuerySuppliers: contactCardQuerySuppliers,
|
||||
contactCardListSuppliers: contactCardListSuppliers,
|
||||
}
|
||||
|
||||
for w := 1; w <= workerPoolSize; w++ {
|
||||
|
||||
Reference in New Issue
Block a user