+ return nil, fmt.Errorf("read line of %s: %w", journalDayName(place.day), err)
+ }
+
+ if len(line) == 0 || line[len(line)-1] != '\n' {
+ return nil, fmt.Errorf("line of %s at %d does not end its record", journalDayName(place.day), place.offset)
+ }
+
+ return line[:len(line)-1], nil
+}
+
+// Close closes the open day files.
+func (j *Journal) Close() error {
+ err := j.closeWriter()
+ for day, reader := range j.readers {
+ err = errors.Join(err, reader.Close())
+
+ delete(j.readers, day)
+ }
+
+ return err
+}
+
+// Scan gives each line of the day files from place on to add, with its
+// position, oldest day first. The zero position starts at the first line. A
+// crash during a write can leave a part of a line at the end of the last day
+// file. Its write never returned, so Scan cuts it off.
+func (j *Journal) Scan(ctx context.Context, place Position, add func([]byte, Position) error) error {
+ paths, err := filepath.Glob(filepath.Join(j.dir, "*.jsonl"))
+ if err != nil {
+ return fmt.Errorf("list day files: %w", err)
+ }
+
+ slices.Sort(paths)
+
+ for n, path := range paths {
+ day, err := journalDayNumber(strings.TrimSuffix(filepath.Base(path), ".jsonl"))
+ if err != nil {
+ return err
+ }
+
+ if day < place.day {
+ continue
+ }
+
+ offset := uint64(0)
+ if day == place.day {
+ offset = place.offset
+ }
+
+ err = journalScanFile(ctx, path, day, offset, n == len(paths)-1, add)
+ if err != nil {
+ return err
+ }