Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/state-pg-set-if-not-exists-ttl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@chat-adapter/state-pg": patch
---

Fix `setIfNotExists()` so it can claim a cache key whose existing row has expired. Previously the query used `ON CONFLICT DO NOTHING`, so an expired row in `chat_state_cache` still blocked acquisition until opportunistic cleanup deleted it — diverging from the memory and Redis adapters, which treat expired entries as absent. Keys stored without a TTL remain permanent and are never overwritten.
14 changes: 14 additions & 0 deletions packages/state-pg/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,20 @@ describe("PostgresStateAdapter", () => {
expect(result).toBe(true);
});

it("should allow setIfNotExists to replace expired keys", async () => {
queryRows = [{ cache_key: "key" }];
const result = await adapter.setIfNotExists("key", "value", 5000);

expect(result).toBe(true);
expect(pool.query).toHaveBeenCalledWith(
expect.stringContaining(
`WHERE chat_state_cache.expires_at IS NOT NULL
AND chat_state_cache.expires_at <= now()`
),
["chat-sdk", "key", '"value"', expect.any(Date)]
);
});

it("should delete a value without throwing", async () => {
await adapter.delete("key");
});
Expand Down
7 changes: 6 additions & 1 deletion packages/state-pg/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,12 @@ export class PostgresStateAdapter implements StateAdapter {
const result = await this.pool.query(
`INSERT INTO chat_state_cache (key_prefix, cache_key, value, expires_at)
VALUES ($1, $2, $3, $4)
ON CONFLICT (key_prefix, cache_key) DO NOTHING
ON CONFLICT (key_prefix, cache_key) DO UPDATE
SET value = EXCLUDED.value,
expires_at = EXCLUDED.expires_at,
updated_at = now()
WHERE chat_state_cache.expires_at IS NOT NULL
AND chat_state_cache.expires_at <= now()
RETURNING cache_key`,
[this.keyPrefix, key, serialized, expiresAt]
);
Expand Down