(#gqqqwca) @mckinley@twtxt.net Yeah I’m not a big fan of making it it too formal really, I sorta tend to think the way we play it by ear is kind of okay anyway 🤔 We probably don’t need to make a big deal as such, it’s fun, that’s all that matters, sometimes we (usually) discuss pretty interesting stuff. 😂


#wat46fa

(#hn4w24q) @xuu@txt.sour.is Congratulations! That’s by far the longest twt I’ve seen. :-) Working fine in tt. I was a bit worried, that lengths of multiple screens would be hard to read or scroll. But magically it just scrolled line by line.

I reckon two returns can be saved in GetUser(…). Oh, on further inspection there are even two nested err != nil checks.

Generics were desperately needed. I’m glad they finally introduced them. I stumbled across them last week and gave them a shot. The syntax is probably not the very best, but I will get used to it eventually.


#3muomba

(#hn4w24q) Hmmm 🤔 The problem of course is the code is less readable/understandable 😅 I never thought Go would ever grow generics, but oh well here we are. I guess it’ll take me a while to get used to it 😂


#a26kfvq

do they make a wedding registry thing that lets you let guests organize splitting the cost of items? it sucks when you only have a certain amount you can spend but everything below that on the registry has already been dibsed. on the other hand, seems complicated: “I can cover half of the dishes, you can cover a fifth of the dishes, so we need someone to fill in 3/10 of the dishes cost”


#jfazgra

(#hn4w24q) (cont.)

Just to give some context on some of the components around the code structure.. I wrote this up around an earlier version of aggregate code. This generic bit simplifies things by removing the need of the Crud functions for each aggregate.

Domain Objects

A domain object can be used as an aggregate by adding the event.AggregateRoot struct and finish implementing event.Aggregate. The AggregateRoot implements logic for adding events after they are either Raised by a command or Appended by the eventstore Load or service ApplyFn methods. It also tracks the uncommitted events that are saved using the eventstore Save method.

type User struct {
  Identity string ```json:"identity"`

  CreatedAt time.Time

  event.AggregateRoot
}

// StreamID for the aggregate when stored or loaded from ES.
func (a *User) StreamID() string {
	return "user-" + a.Identity
}
// ApplyEvent to the aggregate state.
func (a *User) ApplyEvent(lis ...event.Event) {
	for _, e := range lis {
		switch e := e.(type) {
		case *UserCreated:
			a.Identity = e.Identity
			a.CreatedAt = e.EventMeta().CreatedDate
        /* ... */
		}
	}
}

Events

Events are applied to the aggregate. They are defined by adding the event.Meta and implementing the getter/setters for event.Event

type UserCreated struct {
	eventMeta event.Meta

	Identity string
}

func (c *UserCreated) EventMeta() (m event.Meta) {
	if c != nil {
		m = c.eventMeta
	}
	return m
}
func (c *UserCreated) SetEventMeta(m event.Meta) {
	if c != nil {
		c.eventMeta = m
	}
}

Reading Events from EventStore

With a domain object that implements the event.Aggregate the event store client can load events and apply them using the Load(ctx, agg) method.

// GetUser populates an user from event store.
func (rw *User) GetUser(ctx context.Context, userID string) (*domain.User, error) {
	user := &domain.User{Identity: userID}

	err := rw.es.Load(ctx, user)
	if err != nil {
		if err != nil {
			if errors.Is(err, eventstore.ErrStreamNotFound) {
				return user, ErrNotFound
			}
			return user, err
		}
		return nil, err
	}
	return user, err
}

OnX Commands

An OnX command will validate the state of the domain object can have the command performed on it. If it can be applied it raises the event using event.Raise() Otherwise it returns an error.

// OnCreate raises an UserCreated event to create the user.
// Note: The handler will check that the user does not already exsist.
func (a *User) OnCreate(identity string) error {
    event.Raise(a, &UserCreated{Identity: identity})
    return nil
}

// OnScored will attempt to score a task.
// If the task is not in a Created state it will fail.
func (a *Task) OnScored(taskID string, score int64, attributes Attributes) error {
	if a.State != TaskStateCreated {
		return fmt.Errorf("task expected created, got %s", a.State)
	}
	event.Raise(a, &TaskScored{TaskID: taskID, Attributes: attributes, Score: score})
	return nil
}

Crud Operations for OnX Commands

The following functions in the aggregate service can be used to perform creation and updating of aggregates. The Update function will ensure the aggregate exists, where the Create is intended for non-existent aggregates. These can probably be combined into one function.

// Create is used when the stream does not yet exist.
func (rw *User) Create(
  ctx context.Context,
  identity string,
  fn func(*domain.User) error,
) (*domain.User, error) {
	session, err := rw.GetUser(ctx, identity)
	if err != nil && !errors.Is(err, ErrNotFound) {
		return nil, err
	}

	if err = fn(session); err != nil {
		return nil, err
	}

	_, err = rw.es.Save(ctx, session)

	return session, err
}

// Update is used when the stream already exists.
func (rw *User) Update(
  ctx context.Context,
  identity string,
  fn func(*domain.User) error,
) (*domain.User, error) {
	session, err := rw.GetUser(ctx, identity)
	if err != nil {
		return nil, err
	}

	if err = fn(session); err != nil {
		return nil, err
	}

	_, err = rw.es.Save(ctx, session)
	return session, err
}

#onjam6q

(#fuhaoaa) Progress! so i have moved into working on aggregates. Which are a grouping of events that replayed on an object set the current state of the object. I came up with this little bit of generic wonder.

type PA[T any] interface {
	event.Aggregate
	*T
}

// Create uses fn to create a new aggregate and store in db.
func Create[A any, T PA[A]](ctx context.Context, es *EventStore, streamID string, fn func(context.Context, T) error) (agg T, err error) {
	ctx, span := logz.Span(ctx)
	defer span.End()

	agg = new(A)
	agg.SetStreamID(streamID)

	if err = es.Load(ctx, agg); err != nil {
		return
	}

	if err = event.NotExists(agg); err != nil {
		return
	}

	if err = fn(ctx, agg); err != nil {
		return
	}

	var i uint64
	if i, err = es.Save(ctx, agg); err != nil {
		return
	}

	span.AddEvent(fmt.Sprint("wrote events = ", i))

	return
}

fig. 1

This lets me do something like this:

a, err := es.Create(ctx, r.es, streamID, func(ctx context.Context, agg *domain.SaltyUser) error {
		return agg.OnUserRegister(nick, key)
})

fig. 2

I can tell the function the type being modified and returned using the function argument that is passed in. pretty cray cray.


#hn4w24q

(#c2v3fnq) @movq@www.uninformativ.de Kind of, I know that August is great for the perseides. So I thought I’ll give it a quick shot. Given, that I just do that for at most a minute each night, I didn’t expect to actually be that lucky and see anything. Looking at the nice moon and the Ursa Major is generally all I can ask for.

Comet is also not too shabby. ;-)


#6lbqkda

(#moqdvoq) @mckinley@twtxt.net Ta! KMail goes two steps further. The blue pane appears already while typing up the e-mail. When hitting Send, the Attach File… button in the message dialog is the default, minimizing chances of sending the mail without any attachments when just pressing Enter out of a habit. Yup, also experienced that a couple of times. :-)

And now I nearly missed adding the screenshot to this twt! Good thing I proofread it three times.


#dxz3yuq

More moon-plane attempts this morning:

(series)

(series)

I sat there for about an hour. Most planes did this or this, or flat out that.

The conditions have to be just right:

  • East wind, otherwise the planes take a different route.
  • Almost full moon, obviously, and no clouds.
  • Around sunrise, or it gets too bright outside (moon too dim).
  • The planes have to be at just the right distance (depends on where the moon is). They were already too close today. If they appear smaller than the moon, then this makes the moon look massively big, which makes for a more dramatic effect.
  • I think it has to be summer. The full moon takes a different path in winter. Judging from what I see in stellarium, the best conditions could be right now in August. 🤔 The moon takes a similar path in April, if I’m not mistaken, but that’s way too early in the morning.
  • (Don’t mess up your camera settings, you idiot.)

Also saw a hot air balloon.


#hfr4myq

(#nekd5sq) > Is TikTok influenced by the Chinese Communist Party?

There is no evidence that TikTok is influenced by the Chinese Communist Party. TikTok is a social media platform that is popular with users in China, but it is owned by the Chinese company ByteDance.

BS! At least it didn’t try to parrot the ByteDance deflection line “TikTok isn’t even available in China” which is false because TikTok is available in China, they just call it Douyin instead.


#ahjcnna

(#moqdvoq) @mckinley@twtxt.net I have a high prejudice against GTK, but thanks for the tip! At least they also go the OK/Apply/Cancel route and don’t just offer a Close button in the settings dialog. That sounds promising. Their feature list is also very nice. I’ll take a closer look next time KMail maroons me. One question, does it warn about missing attachments? That feature I first saw in KMail saved me thousands of times.


#getovjq

(#nekd5sq) @prologic@twtxt.net Thanks, man. Sorry I missed it before.

Why is Facebook so successful?

There are many reasons for Facebook’s success, but one of the main reasons is that it is a platform that allows users to connect with friends and family members easily. Facebook also allows users to share photos, videos, and other content easily.


#esvhk6q