# NATS Lua Implementation - Deep Analysis and Fix Plan

## Executive Summary

The Lua NATS implementation had several issues including socket timeout handling and message parsing. Root cause was misunderstanding of Go/C concurrency model and incomplete NATS protocol implementation.

`★ Insight ─────────────────────────────────────`
**Current Status:**

- Socket timeout is 3 seconds, which is adequate for timeout detection
- Blocking approach works fine with proper socket timeout
- Fixed CRLF parsing bug - now always waits for complete NATS messages
- Timeout detection: request expiry checked every 3s (worst case +3s over target)
- Heartbeat detection: checked every 3s (acceptable for 10s heartbeat timeout)

**Issues Fixed:**

1. ✅ CRLF parsing bug - was using `waitData = -1` after first read, causing partial reads
2. ✅ Socket timeout handling - now properly handles `sock:receive()` timeout returns
3. ⏳ Pull consumer timeout precision - works but could use non-blocking for exact timing
4. ⏳ Heartbeat detection - needs "100 Idle Heartbeat" message detection

**Go vs Lua Concurrency Models:**

| Feature | Go (pull.go:933-1003) | C (js.c) | Lua (current) |
|---------|----------------------|----------|---------------|
| Execution | Goroutines (parallel) | OS Threads | Coroutines (cooperative single-thread) |
| Message Wait | `select` checks ALL channels simultaneously | `natsCondition_TimedWait` | `sock:receive()` blocks |
| Heartbeat Monitor | `time.AfterFunc` in separate goroutine sends to error channel | Background thread with timer | Checked AFTER blocking read returns |
| Timeout Check | `time.After()` channel fires independently | `natsCondition_TimedWait` timeout | Checked AFTER blocking read returns |

**Go's select loop (pull.go:937-999):**

```go
select {  // Checks ALL channels simultaneously
case msg := <-msgs:        // Message arrives
case err := <-sub.errs:    // Heartbeat timeout (from timer goroutine)
case <-time.After(...):    // Request expiry
case <-ctxDone:            // Context cancellation
}
```

**Lua's blocking call (current):**

```lua
while not done do
    nc:processIncoming()  -- BLOCKS - cannot check timeouts while waiting
    -- Checks only happen AFTER blocking read returns
end
```

**Lua needed fix - NON-BLOCKING POLLING:**

```lua
while not done do
    nc:processIncoming({check_connected = false})  -- Non-blocking, returns immediately
    -- Check timeouts FREQUENTLY
    if util.seconds(startTime) > requestExpiry then break end
    if util.seconds(lastHeartbeatTime) > heartbeatTimeout then break end
    util.sleep(0.001)  -- 1ms sleep, yield to other coroutines
end
```

`─────────────────────────────────────────────────`

---

## Source References

| Component | Go Source | C Source | Lua Implementation |
|-----------|-----------|----------|-------------------|
| Pull Consumer | `nats.go/jetstream/pull.go:900-1003` | `nats.c/src/js.c:...` | `nats-pull-consumer.lua` |
| Ordered Consumer | `nats.go/jetstream/ordered.go:285-356` | - | `nats-ordered-consumer.lua` |
| Object Store | `nats.go/jetstream/object.go:827-944` | `nats.c/src/object.c:...` | `nats-object-store.lua` |
| Base NATS | `nats.go/nats.go` | `nats.c/src/conn.c` | `nats.lua` |

---

## Critical Issues with Go/C Line References

### Issue 0: Socket Receive - CRLF Parsing Bug (FIXED ✅)

**Location:** `nats.lua:300-332`

**Problem:**

After the first `sock:receive(api.CRLF)` call, `waitData` was set to `-1`, causing subsequent reads to return partial data without waiting for CRLF message boundaries.

**Buggy Code:**

```lua
if wait then
    waitData = api.CRLF  -- First read: waits for CRLF
else
    waitData = -1
end
repeat
    readData, sockErr, readLen = self.sock:receive(waitData)
    -- ... parse data ...
    waitData = -1  -- BUG: Subsequent reads don't wait for CRLF!
until false
```

**Impact:**

- Huge messages could return partial data like "abcabc" without message boundaries
- Parser couldn't handle partial data correctly
- Caused parsing errors and connection issues

**NATS Protocol Format:**
All NATS messages end with `\r\n` (CRLF):

```text
MSG <subject> <sid> [reply-to] <#bytes>\r\n<payload>\r\n
PING\r\n
PONG\r\n
+OK\r\n
-ERR <error message>\r\n
INFO <json>\r\n
```

**Fix Applied:**

```lua
local readData, sockErr, parserState, readLen, waitData
-- NATS protocol messages always end with CRLF, so always wait for CRLF
waitData = api.CRLF
repeat
    readData, sockErr, readLen = self.sock:receive(waitData, true)
    -- Handle timeout case
    if readLen == 0 and sockErr == "timeout" then
        return
    end
    -- ... parse data ...
    -- Keep waitData as api.CRLF for all iterations
until false
```

**Result:** `sock:receive()` now always waits for complete NATS messages ending with CRLF, preventing partial reads.

---

### Issue 1: Pull Consumer - Blocking Socket Read (CRITICAL)

**Location:** `nats-pull-consumer.lua:140-168`

**Current Lua Code:**

```lua
-- Go: pull.go:900-1003 fetch()
while not done do
    nc:processIncoming()
    if done then break end
    if nc.error.code ~= E.codeTimeout then
        lastStatus = nc.error
        break
    end
    -- timeout check happens AFTER blocking read returns
    if util.seconds(startTime) > timeout + 1 then
        lastStatus = "timeout"
        break
    end
    -- heartbeat check happens AFTER blocking read returns
    if heartbeatTimeout and util.seconds(lastHeartbeatTime) > heartbeatTimeout then
        lastStatus = "heartbeat timeout"
        break
    end
end
```

**Go Reference (pull.go:933-1003):**

```go
// pull.go:933-1003 - fetch() with NON-BLOCKING select loop
go func(res *fetchResult) {
    defer sub.subscription.Unsubscribe()
    defer close(res.msgs)
    for {
        select {  // NON-BLOCKING - checks all channels
        case msg := <-msgs:  // Line 938 - non-blocking channel read
            // process message...
        case err := <-sub.errs:  // Line 982 - error channel
            res.err = err
            return
        case <-time.After(req.Expires + 1*time.Second):  // Line 988 - timeout channel
            res.done = true
            return
        case <-ctxDone:  // Line 993 - context cancellation
            res.err = req.ctx.Err()
            return
        }
    }
}(res)
```

**Current Status:** ✅ WORKS (with CRLF fix applied)

The blocking approach works fine with socket connect_timeout = 3, default_timeout = 0.5 seconds:

- Socket timeout fires every 3 seconds, allowing timeout checks
- Request expiry checked every 3s (worst case: +3s over target)
- Heartbeat timeout checked every 3s (acceptable for 10s heartbeat timeout)
- CRLF fix ensures complete messages are received

**Note:** With `waitData = api.CRLF` always set, `sock:receive()` waits for complete NATS messages ending with CRLF, preventing partial reads.

**Precision vs Correctness:**

- **Current approach:** Correct but imprecise (worst case +3s)
- **Non-blocking approach:** Precise timeout (exactly when expiry reached)

For most use cases, +3s worst case is acceptable. Non-blocking loop would provide exact timing for time-critical applications.

**C Reference (js.c:pull consumer):**

- Uses `natsThread_Create` for background threads
- Uses `natsMutex_Lock/Unlock` for state protection
- Uses `natsCondition_Wait/Signal` for coordination

**Fix:** Use `processIncoming({check_connected = false})` with small sleep intervals to check conditions while waiting:

```lua
-- Go: pull.go:988-993 - timeout from request start, NOT last message
local requestStartTime = util.seconds()
local requestExpiry = (opt.expires_seconds or self.js.opt.timeout or 30) + 1
local heartbeatTimeout = opt.idle_heartbeat_seconds and opt.idle_heartbeat_seconds * 2

while not done do
    -- NON-BLOCKING: check conditions frequently
    nc:processIncoming({check_connected = false})  -- false = don't wait for data

    -- Go: pull.go:988 - timeout from request start
    if util.seconds(requestStartTime) > requestExpiry then
        return self:err(E.codeTimeout, "request expired")
    end

    -- Go: pull.go:1068-1077 - heartbeat check (2x heartbeat interval)
    if heartbeatTimeout and util.seconds(lastHeartbeatTime) > heartbeatTimeout then
        return self:err(E.codeTimeout, "heartbeat timeout")
    end

    -- yield to allow other coroutines to run
    util.sleep(0.001)  -- 1ms sleep
end
```

---

### Issue 2: Pull Consumer - Missing Heartbeat Monitor (CRITICAL)

**Location:** `nats-pull-consumer.lua:63-92`

**Current Lua Code:**

```lua
local function callback(msg)
    lastHeartbeatTime = util.seconds()  -- Line 67 - reset on message
    if msg.header and next(msg.header) then
        local status = msg.header.Status
        if status == "404" or status == "409" or status == "408" then
            done = true
            lastStatus = status
        end
    -- NO HEARTBEAT DETECTION FOR "100 Idle Heartbeat" MESSAGES
    end
end
```

**Go Reference (pull.go:250-268, 656-659):**

```go
// pull.go:250-268 - internalHandler with heartbeat processing
internalHandler := func(msg *nats.Msg) {
    if sub.hbMonitor != nil {
        sub.hbMonitor.Stop()  // Line 252 - stop existing monitor
    }
    userMsg, msgErr := checkMsg(msg)  // Line 254 - check if user message
    if !userMsg && msgErr == nil {
        // Line 256-258 - heartbeat message, reset monitor
        if sub.hbMonitor != nil {
            sub.hbMonitor.Reset(2 * consumeOpts.Heartbeat)
        }
        return
    }
    // ... process user message
}

// pull.go:1068-1077 - scheduleHeartbeatCheck creates timer
func (s *pullSubscription) scheduleHeartbeatCheck(dur time.Duration) *hbMonitor {
    if dur == 0 {
        return nil
    }
    return &hbMonitor{
        timer: time.AfterFunc(2*dur, func() {  // Line 1073 - 2x heartbeat duration
            s.errs <- ErrNoHeartbeat  // Line 1074 - send to error channel
        }),
    }
}
```

**Problem:** Lua doesn't detect "100 Idle Heartbeat" messages and doesn't monitor heartbeat timeout.

**Fix:** Add heartbeat detection in callback:

```lua
-- Go: pull.go:254, 663-666 - heartbeat message detection
local function callback(msg)
    lastHeartbeatTime = util.seconds()  -- Go: pull.go:657/659 - reset on any message

    -- Go: pull.go:661-672 - check for status messages
    if msg.header and next(msg.header) then
        local status = msg.header.Status
        if status == "404" or status == "409" or status == "408" then
            done = true
            lastStatus = status
        elseif status == "100" then  -- "100 Idle Heartbeat" or "100 FlowControl Request"
            -- Heartbeat detected, will be handled by timeout check
        end
    elseif msg.data:sub(1, 12) == "NATS/1.0 100" then
        -- Heartbeat in payload (older servers)
        -- lastHeartbeatTime already reset above
    else
        received = received + 1
        receivedMsgs[received] = msg
        if received >= needed then
            done = true
        -- ... pending count check
        end
    end
end
```

---

### Issue 3: Ordered Consumer - Infinite Loop Without Timeout (CRITICAL)

**Location:** `nats-ordered-consumer.lua:143-204`

**Current Lua Code:**

```lua
-- Go: ordered.go:285-356 orderedSubscription.Next()
function S.next()
    while true do  -- Line 150 - NO timeout check!
        msg = S.underlyingMessages:next()
        if msg and msg.error then
            if msg.error.code == E.codeTimeout then
                return msg  -- Good: propagate timeout
            end
            err = S.consumer:reset()
            if err then
                return err
            end
            S.underlyingMessages = S.consumer.currentConsumer:messages(S.opt)
            if S.underlyingMessages.error then
                return S.underlyingMessages
            end
            -- NO timeout check here - continues immediately!
        elseif msg == nil then
            S.closed = true
            return nil
        else
            -- sequence validation...
        end
    end
end
```

**Go Reference (ordered.go:285-356):**

```go
// ordered.go:285-356 - Next() with timeout propagation
func (s *orderedSubscription) Next(opts ...NextOpt) (Msg, error) {
    for {  // Line 286 - simple for loop, NO goto
        msg, err := s.consumer.currentSub.Next(opts...)
        if err != nil {
            // Line 294-296 - propagate timeout WITHOUT reset
            if errors.Is(err, nats.ErrTimeout) {
                return nil, err  // CRITICAL: return immediately!
            }
            if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
                return nil, err  // Line 297-299 - propagate context errors
            }
            // Line 315-320 - reset for other errors
            if err := s.consumer.reset(); err != nil {
                return nil, err
            }
            cc, err := s.consumer.currentConsumer.Messages(s.opts...)
            if err != nil {
                return nil, err
            }
            s.consumer.currentSub = cc.(*pullSubscription)
            continue  // Line 326 - simple continue, no goto
        }
        // ... sequence check and return
        return msg, nil  // Line 354
    }
}
```

**Problem:**

- Go's Next() receives timeout from underlying pull consumer and returns immediately
- Lua's version loops forever on reset without checking overall timeout
- The iteration counter is a band-aid, not a proper solution

**Fix:** Add overall timeout to ordered consumer's Next():

```lua
-- Go: ordered.go:285-356 - with proper timeout handling
function orderedConsumer:messages(opt)
    -- ... existing setup ...
    local overallTimeout = opt.timeout_seconds or 30
    local messagesStart = util.seconds()

    function S.next()
        -- Go: ordered.go:294-296 - check overall timeout FIRST
        if util.seconds(messagesStart) > overallTimeout then
            return self:err(E.codeTimeout, "messages iterator timeout")
        end

        msg = S.underlyingMessages:next()
        if msg and msg.error then
            if msg.error.code == E.codeTimeout then
                return msg  -- Go: ordered.go:295 - propagate timeout
            end
            if msg.error.code == E.codeBadRequest or msg.error.code == E.codeInvalidArgument then
                return msg  -- Go: ordered.go:291-293 - propagate without reset
            end
            -- Go: ordered.go:315-320 - reset for other errors
            err = S.consumer:reset()
            if err then
                return err
            end
            S.underlyingMessages = S.consumer.currentConsumer:messages(S.opt)
            if S.underlyingMessages.error then
                return S.underlyingMessages
            end
            -- continue to next iteration with timeout check
        elseif msg == nil then
            S.closed = true
            return nil
        else
            -- sequence validation (Go: ordered.go:333-354)
            local meta = msg:metadata({"consumer", "stream_sequence", "consumer_sequence"})
            if meta and meta.error then
                return meta
            end
            local serial = serialNumberFromConsumer(meta.consumer)
            if serial == S.consumer.serial then
                local dseq = meta.consumer_sequence
                if dseq == S.consumer.cursor.deliverSeq + 1 then
                    S.consumer.cursor.deliverSeq = dseq
                    S.consumer.cursor.streamSeq = meta.stream_sequence
                    return msg  -- Go: ordered.go:354 - return valid message
                end
                -- Go: ordered.go:338-351 - sequence mismatch, reset
                err = S.consumer:reset()
                if err then
                    return err
                end
                S.underlyingMessages = S.consumer.currentConsumer:messages(S.opt)
                if S.underlyingMessages.error then
                    return S.underlyingMessages
                end
                -- continue to next iteration
            end
            -- serial mismatch - continue to next iteration
        end
    end
end
```

---

### Issue 4: Object Store - Consumer Caching (HIGH)

**Location:** `nats-object-store.lua:287-293, 369-379`

**Current Lua Code:**

```lua
function objectStore:getOrderedConsumer(info, opt)
    local chunkSubject = format(api.objChunksPreTmpl, self.name, info.nuid)
    local oc = self.orderedConsumerCache[chunkSubject]  -- CACHE!
    if oc == nil then
        oc = self.js:orderedConsumer(self.streamName, {filter_subject = chunkSubject})
        if oc.error then
            return oc
        end
        self.orderedConsumerCache[chunkSubject] = oc  -- CACHE!
    end
    -- ... use cached consumer
end

function objectStore:getDirect(info, opt)
    local cacheKey = chunkSubject .. ":" .. fetchOption
    local cons = self.directConsumerCache[cacheKey]  -- CACHE!
    if cons == nil then
        cons = self.js:createOrUpdateConsumer(self.streamName, consumerCfg)
        if cons.error then
            return cons
        end
        self.directConsumerCache[cacheKey] = cons  -- CACHE!
    end
    -- ... use cached consumer
end
```

**Go Reference (object.go:827-944):**

```go
// object.go:827-944 - Get() creates FRESH consumer each time
func (obs *obs) Get(ctx context.Context, name string, opts ...GetObjectOpt) (ObjectResult, error) {
    // ... get info ...

    // Line 876-877 - create NEW pipe for each request
    pr, pw := net.Pipe()
    result.r = pr

    // Line 931-938 - create FRESH ordered consumer, NO caching!
    chunkSubj := fmt.Sprintf(objChunksPreTmpl, obs.name, info.NUID)
    streamName := fmt.Sprintf(objNameTmpl, obs.name)
    subscribeOpts := []nats.SubOpt{
        nats.OrderedConsumer(),  // Creates NEW consumer
        nats.Context(ctx),
        nats.BindStream(streamName),
    }
    _, err = obs.pushJS.Subscribe(chunkSubj, processChunk, subscribeOpts...)
    // ...
}
```

**C Reference (object.c:):**

- C implementation also creates fresh consumer for each Get operation
- No caching of consumers

**Problem:**

- Cached consumers have stale state after first use
- Subsequent Get() calls return wrong data or timeout
- After error, cached consumer is in invalid state

**Fix:** REMOVE consumer caching entirely:

```lua
-- Go: object.go:931-938 - create FRESH consumer for each Get
function objectStore:getOrderedConsumer(info, opt)
    local chunkSubject = format(api.objChunksPreTmpl, self.name, info.nuid)

    -- Go: ordered.go:414-445 - create NEW ordered consumer for each fetch
    local oc = self.js:orderedConsumer(self.streamName, {
        filter_subject = chunkSubject,
        delete_consumer = true  -- Clean up after use
    })
    if oc.error then
        return oc
    end
    -- NO CACHING - create fresh each time
    -- ... use consumer ...
end

function objectStore:getDirect(info, opt)
    local chunkSubject = format(api.objChunksPreTmpl, self.name, info.nuid)
    local newNuid = nuidNext()
    local consumerName = "object-store-direct-" .. newNuid:sub(1, 8)
    local consumerCfg = {
        name = consumerName,
        filter_subject = chunkSubject,
        ack_policy = ackPolicy,
        replay_policy = "instant",
        deliver_policy = deliverPolicy,
        inactive_threshold = 60 * 1e9,  -- 1 minute cleanup
    }
    local cons = self.js:createOrUpdateConsumer(self.streamName, consumerCfg)
    if cons.error then
        return cons
    end
    -- NO CACHING - create fresh each time
    -- ... use consumer ...
    -- Clean up after use
    self.js:deleteConsumer(self.streamName, consumerName)
end
```

---

### Issue 5: Object Store - Hardcoded Fetch Count Limit (MEDIUM)

**Location:** `nats-object-store.lua:389-392`

**Current Lua Code:**

```lua
local maxFetches = chunkCount + 5
while remaining > 0 and fetchCount < maxFetches do
    fetchCount = fetchCount + 1
    if fetchCount > 3 then  -- HARDCODED LIMIT!
        return self:err(E.codeTimeout, format("fetch timeout: got %d/%d chunks after %d fetches", #allMsgs, chunkCount, fetchCount))
    end
    -- ...
end
```

**Go Reference (object.go:888-929):**

```go
// object.go:888-929 - processChunk callback
processChunk := func(m *nats.Msg) {
    // ... process chunk data ...

    // Line 924-928 - check NumPending for completion, NO fetch count limit
    if tokens[parser.AckNumPendingTokenPos] == objNoPending {
        pw.Close()  // Done - all chunks received
        m.Sub.Unsubscribe()
    }
}
```

**Problem:**

- Arbitrary limit of 3 fetches
- No relation to actual chunk count or network conditions
- Should continue until NumPending == 0

**Fix:** Remove hardcoded limit, use only timeout and NumPending:

```lua
-- Go: object.go:924-928 - completion detected by NumPending, NO fetch limit
while remaining > 0 do
    local msgs = cons:fetch(remaining, opt)
    if msgs.error then
        if msgs.error.code == E.codeTimeout then
            -- Check if we have all chunks
            local info = cons:info()
            if info and info.num_pending == 0 then
                break  -- Done - all chunks received
            end
        end
        return msgs
    end
    if #msgs == 0 then
        break
    end
    -- ... process messages ...
end
```

---

### Issue 6: getDirect Not Using Direct Get API (MEDIUM)

**Location:** `nats-object-store.lua:332-450`

**Current Lua Code:**

```lua
function objectStore:getDirect(info, opt)
    -- Uses createOrUpdateConsumer + fetch, NOT the direct get API
    local cons = self.js:createOrUpdateConsumer(self.streamName, consumerCfg)
    local msgs = cons:fetch(remaining, opt)
    -- ...
end
```

**Go Reference (stream.go):**

- Go has `GetMsg` and `GetLastMsgForSubject` for direct message retrieval
- These use "Direct Get" API which doesn't require consumer creation

**Problem:** The "getDirect" function doesn't actually use the Direct Get API - it creates a consumer and fetches.

**Fix:** Use Direct Get API if available:

```lua
-- Use Direct Get API (getmsg) for single-chunk objects
if info.chunks == 1 then
    local directResult = self.js:getMsg(self.streamName, info.nuid, {
        subject = chunkSubject
    })
    if directResult and not directResult.error then
        return {data = directResult.data, info = info}
    end
end
-- Fallback to consumer-based fetch for multi-chunk
```

---

## Implementation Plan

### Completed ✅

**Phase 0: Fix Socket Receive CRLF Parsing**

- **File:** `nats.lua:289-332`
- **Status:** ✅ FIXED
- **Changes:**
  1. Removed `waitData = -1` assignment after first read
  2. Always use `waitData = api.CRLF` (all NATS messages end with CRLF)
  3. Added proper timeout handling: `if readLen == 0 and sockErr == "timeout" then return end`
  4. Added `sock:receive(waitData, true)` with buffer read parameter

### Remaining Work

**Phase 1: Fix Pull Consumer (HIGH PRIORITY)**

**File:** `nats-pull-consumer.lua`

**Status:** ⏳ Works but could improve precision

**Current Behavior:** With socket timeout = 3s, timeouts are detected every 3s. Worst case adds +3s to expiry.

**Optional Changes (for exact timing):**

1. Use `processIncoming({check_connected = false})` with 1ms sleep for precise timeout detection
2. Add "100 Idle Heartbeat" message detection in callback

**Note:** Current implementation is functional. Non-blocking loop would provide exact timeout timing but current +3s worst case is acceptable.

**Phase 2: Fix Ordered Consumer (MEDIUM PRIORITY)**

**File:** `nats-ordered-consumer.lua`

**Changes:**

1. Add overall timeout to messages iterator
2. Properly propagate timeout errors without reset (Go ordered.go:294-296)
3. Remove iteration counter, use proper timeout instead

**Phase 3: Fix Object Store (MEDIUM PRIORITY)**

**File:** `nats-object-store.lua`

**Changes:**

1. REMOVE consumer caching entirely (Go creates fresh each time)
2. Remove hardcoded fetch count limit (Go uses NumPending, not fetch count)
3. Add proper NumPending check for completion detection

---

## Testing Plan

```bash
# Run performance test
cd /Volumes/nc/nc-server
lj test/lib/nats-performance-test.lua

# Expected results after fixes:
# - All 14 tests pass
# - 400+ objects/sec on get operations
# - No timeout errors (or rare +3s worst case)
# - No socket disconnections
# - No CRLF parsing errors
```

---

## Success Criteria

1. ✅ **CRLF message parsing** - always wait for complete NATS messages
2. ✅ **Socket timeout handling** - properly handle `sock:receive()` timeout returns
3. ✅ **Request-based timeouts** - timeout from request start (works with 3s checks)
4. ⏳ **Heartbeat detection** - detect "100 Idle Heartbeat" messages (optional)
5. ⏳ **No consumer caching** - create fresh consumers for each operation (object store)
6. ⏳ **No fetch limits** - rely only on NumPending and timeouts (object store)

---

## Files Modified / To Modify

| File | Status | Changes |
|------|--------|---------|
| `nats.lua` | ✅ FIXED | CRLF parsing, socket timeout handling |
| `nats-pull-consumer.lua` | ⏳ Optional | Non-blocking for exact timing (currently works) |
| `nats-ordered-consumer.lua` | ⏳ TODO | Fix timeout propagation, remove iteration limit |
| `nats-object-store.lua` | ⏳ TODO | Remove caching, remove fetch limit |
