circuits-agent
Safe HaskellNone
LanguageGHC2024

Cursor

Description

A position in an append-only log of lines.

Two storage backends, one type:

  • newMemIORef (ephemeral; dies with the process)
  • newFile — file holding a decimal line count (survives restart)

Both answer the same question: what is new since I last asked?

This module has no dependency on muster or circuits-io. Either consumer can hold a Cursor and call pollLines / pollFile without caring where the position lives.

Line index convention

Positions are zero-based counts of complete lines (same as length (T.lines content) when every record ends in \n, and same as wc -l). pollLines advances to length xs after returning the suffix.

Synopsis

Documentation

data Cursor Source #

Opaque read position in a line-oriented log.

Construct with newMem or newFile. Readwrite with getset. Advance with pollLines (in-memory log) or pollFile (path to log).

newMem :: Int -> IO Cursor Source #

In-memory cursor (IORef). Position dies with the process.

>>> c <- newMem 0
>>> get c
0
>>> pollLines c ["a", "b" :: Text]
["a","b"]
>>> pollLines c ["a", "b", "c"]
["c"]
>>> get c
3

newFile :: FilePath -> IO Cursor Source #

File-backed cursor. Contents are a decimal integer plus newline (muster-compatible: show n <> "\n"). Missing file reads as 0; first set creates it.

get :: Cursor -> IO Int Source #

Current line position.

set :: Cursor -> Int -> IO () Source #

Set line position (does not touch the log).

pollLines :: Cursor -> [Text] -> IO [Text] Source #

Given the full current log as lines, return those after the cursor and advance the cursor to length xs.

Idempotent on a frozen log: a second call with the same xs yields [].

If the cursor is past the end of the current content (for example because the log was deleted or truncated), it resets to 0 and the current content is returned as new. This keeps cursors from becoming permanently stale.

>>> c <- newMem 0
>>> pollLines c ["x" :: Text]
["x"]
>>> pollLines c ["x"]
[]

pollFile :: Cursor -> FilePath -> IO [Text] Source #

Read a log file as lines (lines), then pollLines.

Missing file → empty log. Empty file → empty log.

Partial last lines (no trailing newline) are kept as a final element of lines only when content is non-empty and does not end in \n — actually lines drops a trailing empty segment, so a file ending without \n still yields its last partial line. Prompt-style partial lines are therefore visible to the cursor; completeness is the caller's concern (prompt detection lives above this layer).

pollNumberedFile :: Cursor -> FilePath -> IO [(Int, Text)] Source #

Like pollFile, but consumes only newline-terminated lines and returns absolute 1-based line numbers alongside.

A partial trailing line (file not ending in \n) is left unconsumed: the cursor stays before it, so once completed the line is delivered exactly once, on a later poll. This is the completeness discipline pollFile leaves to the caller, made the default. Truncation resets to 0, same as pollLines.

seekEnd :: Cursor -> [Text] -> IO () Source #

Move the cursor to the end of the given lines without returning them. Attach pattern: start at "now" so the next poll only sees future output.

>>> c <- newMem 0
>>> seekEnd c ["old" :: Text, "history"]
>>> pollLines c ["old", "history", "new"]
["new"]

seekEndFile :: Cursor -> FilePath -> IO () Source #

seekEnd for a log path.

readLogLinesComplete :: FilePath -> IO [Text] Source #

readLogLines restricted to newline-terminated records: a partial trailing line (content not ending in \n) is excluded.