X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/baf7a940be8927278e3b4b87e7366cebfc16532f..2ad578a24f252706976cddb41bee6cc969f251cd:/log.go?ds=sidebyside diff --git a/log.go b/log.go index d34f459..6d0d478 100644 --- a/log.go +++ b/log.go @@ -58,9 +58,9 @@ func openLog(ctx context.Context, root string, clock func() time.Time) (*Log, er } // Log is the append-only message log of one chat. It writes one JSON line for -// each message to main/YYYY-MM-DD.jsonl, named for the local day of the write. -// It never edits or deletes a line. Each line goes to the operating system -// before Append returns, so a crash of the process loses no message. An +// each message to main/YYYY-MM-DD.jsonl, named for the local day of the +// message. It never edits or deletes a line. Each line goes to the operating +// system before Append returns, so a crash of the process loses no message. An // exclusive lock on the lock file makes sure that only one process writes the // log. After a failed write, the end of the file is not known, so the log // refuses all writes until it is opened again. The caller serializes all calls. @@ -96,26 +96,45 @@ func (l *Log) Close() error { // Append writes a new message to the log and returns it with its ID, size, and // date. func (l *Log) Append(kind, text string) (Message, error) { + return l.AppendDated(kind, text, "") +} + +// AppendDated writes a message with date in RFC 3339 format. An empty date uses +// the clock. The day file uses the local day of the given date. +func (l *Log) AppendDated(kind, text, date string) (Message, error) { if l.lock == nil { - return Message{}, os.ErrClosed + return Message{}, fmt.Errorf("append dated message: %w", os.ErrClosed) } if l.failure != nil { return Message{}, fmt.Errorf("log needs a reopen after a failed write: %w", l.failure) } + var err error + now := l.clock() + if date == "" { + date = now.Format(time.RFC3339Nano) + } else { + now, err = time.Parse(time.RFC3339Nano, date) + if err != nil { + return Message{}, fmt.Errorf("parse message date: %w", err) + } + + now = now.In(time.Local) + } + message := Message{ ID: l.index.count, Kind: kind, Text: text, Size: len(text), - Date: now.Format(time.RFC3339Nano), + Date: date, } - err := logValidateMessage(message) + err = logValidateMessage(message) if err != nil { - return Message{}, err + return Message{}, fmt.Errorf("validate dated message: %w", err) } line, err := json.Marshal(message) @@ -158,6 +177,7 @@ func (l *Log) Message(id int) (Message, error) { } var message Message + err = json.Unmarshal(line, &message) if err != nil { return Message{}, fmt.Errorf("decode message %d: %w", id, err) @@ -188,9 +208,10 @@ func (l *Log) catchUp(ctx context.Context) error { return l.journal.Scan(ctx, start, func(line []byte, place Position) error { var message Message + err := json.Unmarshal(line, &message) if err != nil { - return err + return fmt.Errorf("decode log message: %w", err) } if message.ID != l.index.count { @@ -213,9 +234,10 @@ func (l *Log) addIndex(place Position) error { return l.index.Append(record) } -// Message is one entry of the chat log. ID is the permanent position of the -// message in the chat, from 0. Size is the UTF-8 byte length of Text. Date is -// the time of the write in RFC 3339 format. +// Message is one entry of the chat log. ID is its position in the log, from 0. +// A rebuild assigns positions in the merged log. Size is the UTF-8 byte length +// of Text. Date is the message time in RFC 3339 format, supplied at append or +// taken from the clock. type Message struct { ID int `json:"i"` Kind string `json:"kind"`