### Install Dependencies Source: https://github.com/fahreddinozcan/concavex/blob/main/README.md Install project dependencies using pnpm. ```bash pnpm install ``` -------------------------------- ### Run Development Server Source: https://github.com/fahreddinozcan/concavex/blob/main/README.md Start the development server using pnpm. ```bash pnpm dev ``` -------------------------------- ### Redis Search Index Creation Command Example Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md An example of the FT.CREATE command for setting up a Redis Search index on JSON data. It specifies the index name, data type, key prefix, and schema fields. ```redis FT.CREATE tasks_idx ON JSON PREFIX 1 task: SCHEMA $._id AS _id TAG $._creationTime AS _creationTime NUMERIC SORTABLE $.title AS title TEXT $.description AS description TEXT $.completed AS completed TAG $.priority AS priority NUMERIC SORTABLE $.assignee AS assignee TEXT NOSTEM ``` -------------------------------- ### Optimistic Update Example Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Demonstrates how to provide an optimistic update function to the useMutation hook for immediate UI feedback. Updates the cache before the server response. ```typescript const create = useMutation(api.tasks.create, { optimisticUpdate: (cache, args) => { const current = cache.getQuery(api.tasks.list, {}); cache.setQuery(api.tasks.list, {}, [ ...current, { _id: "temp", _creationTime: Date.now(), ...args }, ]); }, }); ``` -------------------------------- ### Redis JSON Command Reference Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Details the core Redis JSON commands for setting, getting, merging, and deleting JSON documents. ```text JSON.SET {key} $ {json} ← insert or replace entire document JSON.GET {key} $ ← get entire document JSON.MERGE {key} $ {partial} ← merge/patch partial fields JSON.DEL {key} ← delete document (or use DEL {key}) ``` -------------------------------- ### Get Operation with Read-Your-Writes Consistency Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Implements the get operation, prioritizing the in-memory cache for read-your-writes consistency. If the document is not in the cache, it fetches from Redis and updates the cache. ```typescript async get(tableName: string, id: string): Promise { const key = `${tableName}:${id}`; if (this.cache.has(key)) return this.cache.get(key); const doc = await redis.json.get(key, "$"); const result = doc?.[0] ?? null; if (result) this.cache.set(key, result); return result; } ``` -------------------------------- ### Increment Likes Mutation Example Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md An example of a mutation function that increments the 'likes' count for a given post. It automatically handles optimistic concurrency control, retrying on version conflicts. ```typescript // User doesn't need to specify transaction mode - it's automatic! export const incrementLikes = mutation({ handler: async (ctx, { postId }) => { const post = await ctx.db.get("posts", postId); await ctx.db.patch("posts", postId, { likes: post.likes + 1 }); // If another user liked the post simultaneously: // → version conflict detected → auto-retry → both likes counted ✅ } }); ``` -------------------------------- ### Toggle Task Completion Mutation Example Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Demonstrates a mutation to toggle the 'completed' status of a task. This mutation also benefits from automatic optimistic concurrency control, ensuring correct state updates during concurrent operations. ```typescript export const toggleTask = mutation({ handler: async (ctx, { id }) => { const task = await ctx.db.get("tasks", id); await ctx.db.patch("tasks", id, { completed: !task.completed }); // Concurrent toggles are handled correctly via version check } }); ``` -------------------------------- ### Optimistic Transaction Example: Toggle Task Source: https://github.com/fahreddinozcan/concavex/blob/main/README.md Demonstrates optimistic concurrency control for the 'toggle' mutation. Concave automatically handles version conflicts and retries. ```typescript // Two users editing the same task simultaneously export const toggle = mutation({ handler: async (ctx, { id }) => { const task = await ctx.db.get("tasks", id); // version: 5 await ctx.db.patch("tasks", id, { completed: !task.completed }); // If version changed → conflict → auto-retry ✅ }, }); ``` -------------------------------- ### QueryBuilder Class Methods Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md The `QueryBuilder` class allows for chaining methods to construct complex queries. It supports filtering, searching, ordering, pagination, and executing the query to collect results, get the first result, or count total results. ```typescript class QueryBuilder { filter(conditions: Partial>): this search(clause: SearchClause): this order(field: keyof T, direction?: "asc" | "desc"): this take(n: number): this skip(n: number): this collect(): Promise // terminal: execute and return array first(): Promise // terminal: take(1) and return first count(): Promise // terminal: return total count } ``` -------------------------------- ### Serve Concave with Redis Configuration Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Initializes the Concave server with Redis and schema configurations. This is the entry point for the backend. ```typescript serveConcave({ redis, schema, functions }) ``` -------------------------------- ### Redis Search Command Reference Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Lists essential Redis Search commands for index creation, searching, and management. Shows how to use the Upstash Redis SDK for these operations. ```text FT.CREATE {index} ON JSON PREFIX 1 {prefix}: SCHEMA {field} {type} ... FT.SEARCH {index} {query} [SORTBY {field} ASC|DESC] [LIMIT {offset} {count}] FT.DROPINDEX {index} FT.INFO {index} ``` ```typescript await redis.sendCommand(["FT.CREATE", indexName, "ON", "JSON", ...]); ``` ```typescript // The new redis.search namespace from Upstash Redis Search await redis.search.createIndex({ ... }); await redis.search.query({ ... }); ``` -------------------------------- ### Optimistic Mutation Execution Flow Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Illustrates the steps involved in a mutation with optimistic transactions, including reading documents, buffering writes, and committing with version checks. Retries occur on version conflicts. ```text ┌─────────────────────────────────────────────────────────────┐ │ Mutation Execution with Optimistic Transactions │ └─────────────────────────────────────────────────────────────┘ Attempt 1: ┌─────────────────────────────────────────────────┐ │ 1. Read documents (remember versions) │ │ task:abc → { _version: 7, likes: 100 } │ │ versionTracker.set("task:abc", 7) │ └─────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────┐ │ 2. Run handler logic (all writes buffered) │ │ ctx.db.patch("task:abc", { likes: 101 }) │ writeBuffer.push({ key, data }) └─────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────┐ │ 3. Commit: Execute Lua CAS script │ │ Check: current version == 7? │ │ ├─ YES → Apply writes, increment version → 8 │ │ └─ NO → Return VERSION_CONFLICT │ └─────────────────────────────────────────────────┘ ↓ ┌──────────────┴──────────────┐ │ │ ✅ Success ❌ Conflict │ │ ▼ ▼ ┌─────────────┐ ┌───────────────────┐ │ Emit │ │ Rollback buffer, │ │ invalidation│ │ retry (attempt 2) │ └─────────────┘ └───────────────────┘ │ └──→ (repeat flow) After 3 failed attempts → throw VersionConflictError ``` -------------------------------- ### Create Concave API Route Source: https://github.com/fahreddinozcan/concavex/blob/main/README.md Set up the API route to serve Concave functions. This involves importing `serveConcave` and providing the schema and functions. ```typescript // app/api/concave/[...concave]/route.ts import { serveConcave } from "@/src/server"; import schema from "@/concave/schema"; import * as tasks from "@/concave/tasks"; export const { GET, POST } = serveConcave({ schema, functions: { tasks }, }); ``` -------------------------------- ### Upstash Realtime Event Emission and Subscription Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Demonstrates how to emit events from the server and subscribe to them on the client using the Upstash Realtime SDK. ```typescript import { Realtime } from "@upstash/realtime"; import { redis } from "./redis"; const realtime = new Realtime({ schema: eventSchema, redis }); // Server: emit event await realtime.channel("concave:tasks").emit("invalidate", { ... }); ``` ```typescript // Client: subscribe import { useRealtime } from "@upstash/realtime/client"; useRealtime({ event: "concave:tasks", onData: (data) => { ... } }); ``` -------------------------------- ### Concurrent Write Scenario Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Demonstrates how two users concurrently attempting to modify the same data are handled. The first write succeeds, and the second detects a version conflict and retries, ensuring both increments are eventually applied. ```text User A and User B both try to increment likes on the same post: Time │ User A │ User B ──────┼───────────────────────────────┼────────────────────────── T0 │ Read post (v7, likes: 100) │ T1 │ │ Read post (v7, likes: 100) T2 │ Compute new value: 101 │ T3 │ │ Compute new value: 101 T4 │ Commit: check v7 → ✅ write │ │ (version now 8, likes: 101) │ T5 │ │ Commit: check v7 → ❌ conflict! T6 │ │ Retry: Read post (v8, likes: 101) T7 │ │ Compute new value: 102 T8 │ │ Commit: check v8 → ✅ write │ │ (version now 9, likes: 102) Result: Both increments counted correctly ✅ ``` -------------------------------- ### Initialize Concave HTTP Handler Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md The `serveConcave` function initializes Redis and realtime services, sets up authentication, and returns a request handler for Next.js. It supports query, mutation, and subscription actions. ```typescript export function serveConcave(config: { redis?: Redis; schema: SchemaDefinition; functions: Record>; auth?: { getUser: (req: Request) => Promise; }; }) { // 1. Initialize Redis client const redis = config.redis ?? Redis.fromEnv(); // 2. Initialize realtime const realtime = new Realtime({ redis, schema: realtimeSchema }); // 3. Ensure indexes exist (run once on cold start) let indexesReady = false; const ensureReady = async () => { if (!indexesReady) { await ensureIndexes(redis, config.schema); indexesReady = true; } }; // 4. Return handler const handler = async (req: Request) => { await ensureReady(); const url = new URL(req.url); const segments = url.pathname.split("/").filter(Boolean); const action = segments[segments.length - 1]; // "query", "mutate", or "subscribe" // Auth let user = null; if (config.auth) { user = await config.auth.getUser(req); } switch (action) { case "query": { const { fn, args } = await req.json(); const [moduleName, methodName] = fn.split("."); const queryFn = config.functions[moduleName]?.[methodName]; if (!queryFn || queryFn._type !== "query") { return Response.json({ error: "Query not found" }, { status: 404 }); } const ctx = createQueryContext(redis, config.schema, user); const result = await queryFn.handler(ctx, args ?? {}); return Response.json({ data: result, tables: Array.from(ctx._touchedTables) }); } case "mutate": { const { fn, args } = await req.json(); const [moduleName, methodName] = fn.split("."); const mutationFn = config.functions[moduleName]?.[methodName]; if (!mutationFn || mutationFn._type !== "mutation") { return Response.json({ error: "Mutation not found" }, { status: 404 }); } const result = await mutationFn.handler( { redis, schema: config.schema, realtime, user }, args ?? {} ); return Response.json({ data: result }); } case "subscribe": { // SSE endpoint — delegates to @upstash/realtime // Client sends which tables it wants to watch via query params return handleRealtimeSubscription(req, realtime); } default: return Response.json({ error: "Not found" }, { status: 404 }); } }; // Return both GET and POST handlers for Next.js app router return { GET: handler, POST: handler }; } ``` -------------------------------- ### Nanoid for ID Generation Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Shows how to use the nanoid library to generate short, URL-safe, and collision-resistant document IDs. ```typescript import { nanoid } from "nanoid"; const id = nanoid(); // "V1StGXR8_Z5jdHi6B-myT" ``` -------------------------------- ### Project Dependencies Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md List of essential dependencies for the ConcaveX project, including Upstash Redis, Upstash Realtime, and nanoid. ```json { "@upstash/redis": "^1.34.3", "@upstash/realtime": "^0.3.0", "nanoid": "^5.0.9" } ``` -------------------------------- ### Create Query Context Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Returns a context object with a `db` property for database operations. The `db` object provides methods for querying and retrieving documents. ```typescript ctx.db.query(tableName) → returns QueryBuilder ctx.db.get(tableName, id) → returns single document or null ``` -------------------------------- ### Concave Server Route Handler Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Sets up the Next.js API route to serve Concave requests, integrating the schema and task functions. ```typescript import { serveConcave } from "concave/server"; import schema from "@/concave/schema"; import * as tasks from "@/concave/tasks"; export const { GET, POST } = serveConcave({ schema, functions: { tasks }, }); ``` -------------------------------- ### Todo App Query and Mutation Handlers Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Implements query and mutation handlers for task operations including listing, searching, creating, toggling completion, and removing tasks. ```typescript import { query, mutation } from "concave"; export const list = query({ handler: async (ctx, args: { completed?: boolean }) => { let q = ctx.db.query("tasks"); if (args.completed !== undefined) { q = q.filter({ completed: args.completed }); } return q.order("_creationTime", "desc").take(50); }, }); export const search = query({ handler: async (ctx, args: { term: string }) => { return ctx.db .query("tasks") .search({ title: { $smart: args.term } }) .take(20); }, }); export const create = mutation({ handler: async (ctx, args: { title: string }) => { return ctx.db.insert("tasks", { title: args.title, description: "", completed: false, priority: 0, assignee: "", }); }, }); export const toggle = mutation({ handler: async (ctx, args: { id: string }) => { const task = await ctx.db.get("tasks", args.id); if (!task) throw new Error("Task not found"); await ctx.db.patch("tasks", args.id, { completed: !task.completed }); }, }); export const remove = mutation({ handler: async (ctx, args: { id: string }) => { await ctx.db.delete("tasks", args.id); }, }); ``` -------------------------------- ### Direct Document Retrieval using redis.json.get Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Implements `ctx.db.get(tableName, id)` for fast single-document lookups. It uses `redis.json.get` with a direct key lookup, bypassing FT.SEARCH. ```typescript // Use redis.json.get(\"${tableName}:${id}\", \"$\") for direct key lookup ``` -------------------------------- ### Search Compilation to FT.SEARCH Syntax Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Translates search clauses into Redisearch FT.SEARCH queries. Supports smart matching, fuzzy matching, phrase searching, exact matching, and regex. ```plaintext | Search | FT.SEARCH syntax | |---|---| | `{ title: { $smart: "wirless" } }` | Uses Upstash's smart matching pipeline | | `{ title: { $fuzzy: "headphns", distance: 2 } }` | `@title:%%headphns%%` (fuzzy syntax) | | `{ title: { $phrase: "noise cancelling" } }` | `@title:"noise cancelling"` | | `{ title: { $eq: "exact match" } }` | `@title:{exact match}` (TAG-style) | | `{ title: { $regex: "SKU-[0-9]+" } }` | Regex support if available | ``` -------------------------------- ### useQuery Hook for Data Fetching Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Fetches data initially and subscribes to invalidation events via SSE to keep the cache updated. Re-fetches data when relevant tables are invalidated. ```typescript export function useQuery( queryRef: { _type: "query"; name: string; table: string }, args?: Record ): TResult | undefined { const [data, setData] = useState(undefined); const [tables, setTables] = useState([]); const { url } = useConcaveContext(); // 1. Initial fetch useEffect(() => { fetch(`${url}/query`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ fn: queryRef.name, args }), }) .then(res => res.json()) .then(result => { setData(result.data); setTables(result.tables); // which tables this query touched }); }, [queryRef.name, stableStringify(args)]); // 2. Subscribe to invalidation events via SSE useEffect(() => { if (tables.length === 0) return; const eventSource = new EventSource( `${url}/subscribe?tables=${tables.join(",")}` ); eventSource.onmessage = (event) => { const payload = JSON.parse(event.data); if (tables.includes(payload.table)) { // Re-fetch query fetch(`${url}/query`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ fn: queryRef.name, args }), }) .then(res => res.json()) .then(result => setData(result.data)); } }; return () => eventSource.close(); }, [tables.join(",")]); return data; } ``` -------------------------------- ### Environment Variables for Upstash Redis Source: https://github.com/fahreddinozcan/concavex/blob/main/README.md Configure Upstash Redis connection details in the .env file. Ensure Redis Search is enabled for your database. ```env UPSTASH_REDIS_REST_URL=https://your-redis.upstash.io UPSTASH_REDIS_REST_TOKEN=your-token-here ``` -------------------------------- ### Conceptual Optimistic Commit Logic Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md This is a conceptual representation of the optimistic commit logic within the Concavex project's mutation engine. It outlines the core steps: checking versions, returning a conflict error if versions mismatch, and atomically applying writes with an incremented version if all checks pass. This pseudocode illustrates the high-level strategy before the detailed Lua implementation. ```pseudocode -- Check all versions match, then apply all writes atomically for each write: if current_version != expected_version: return VERSION_CONFLICT apply all writes + increment versions ``` -------------------------------- ### Filter Compilation to FT.SEARCH Syntax Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Converts filter objects into Redisearch FT.SEARCH query strings. Supports boolean, number, and string equality, as well as range queries for numbers. ```plaintext | Filter | FT.SEARCH syntax | |---|---| | `{ completed: false }` | `@completed:{false}` | | `{ completed: true }` | `@completed:{true}` | | `{ priority: 5 }` | `@priority:[5 5]` | | `{ assignee: "alice" }` | `@assignee:{alice}` | | `{ priority: { $gt: 5 } }` | `@priority:[(5 +inf]` | | `{ priority: { $gte: 5, $lte: 10 } }` | `@priority:[5 10]` | | Multiple filters | Combined with space (implicit AND) | ``` -------------------------------- ### Concave Functions: List, Create, Toggle Tasks Source: https://github.com/fahreddinozcan/concavex/blob/main/README.md Implement query and mutation functions for managing tasks. The 'list' query fetches tasks with optional filtering, 'create' adds a new task, and 'toggle' updates a task's completion status. ```typescript // concave/tasks.ts import { query, mutation } from "./src"; export const list = query({ handler: async (ctx, args: { completed?: boolean }) => { let q = ctx.db.query("tasks"); if (args.completed !== undefined) { q = q.filter({ completed: args.completed }); } return q.order("_creationTime", "desc").take(50).collect(); }, }); export const create = mutation({ handler: async (ctx, args: { title: string }) => { return ctx.db.insert("tasks", { title: args.title, completed: false, priority: 0, }); }, }); export const toggle = mutation({ handler: async (ctx, args: { id: string }) => { const task = await ctx.db.get("tasks", args.id); await ctx.db.patch("tasks", args.id, { completed: !task.completed, }); }, }); ``` -------------------------------- ### React Integration with Concave Provider Source: https://github.com/fahreddinozcan/concavex/blob/main/README.md Integrate Concave into a React application using `ConcaveProvider`. Use `useQuery` and `useMutation` hooks to interact with backend functions. ```tsx "use client"; import { ConcaveProvider, useQuery, useMutation } from "@/src/react"; function TaskList() { const tasks = useQuery("tasks.list", {}); const create = useMutation("tasks.create"); const toggle = useMutation("tasks.toggle"); return (
{tasks?.map((task) => (
toggle({ id: task._id }) }> {task.completed ? "✅" : "⬜"} {task.title}
))}
); } export default function App() { return ( ); } ``` -------------------------------- ### Insert Operation Implementation Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Implements the insert operation for the mutation context. It generates a unique ID, constructs the full document with metadata, buffers the write command, and updates the in-memory cache. ```typescript async insert(tableName: string, doc: Record): Promise { const id = nanoid(); const key = `${tableName}:${id}`; const fullDoc = { _id: id, _creationTime: Date.now(), ...doc, }; this.writeBuffer.push({ cmd: "json.set", key, value: fullDoc }); this.cache.set(key, fullDoc); this.touchedTables.add(tableName); return id; } ``` -------------------------------- ### Search Tasks by Title Source: https://github.com/fahreddinozcan/concavex/blob/main/README.md Implement a search query function that utilizes Redis Search for fuzzy matching and typo tolerance when searching tasks by title. ```typescript export const search = query({ handler: async (ctx, { term }) => { return ctx.db .query("tasks") .filter({ title: term }) // Smart match handles typos! .take(20) .collect(); }, }); ``` -------------------------------- ### Document Structure with Version Field Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Documents in Concavex automatically include an `_version` field that is managed by the system and incremented on each write. ```json { _id: "task_abc123", _creationTime: 1707687123456, _version: 7, // ← auto-incremented on every write title: "Buy milk", completed: false } ``` -------------------------------- ### Mutation Context Database Operations Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Overview of the database operations available through the mutation context. These operations are part of the interface for interacting with the database within a mutation. ```typescript ctx.db.insert(tableName, doc) → returns id (string) ctx.db.get(tableName, id) → returns document or null ctx.db.patch(tableName, id, fields) → void ctx.db.replace(tableName, id, doc) → void ctx.db.delete(tableName, id) → void ctx.db.query(tableName) → QueryBuilder (same as query context) ``` -------------------------------- ### Lua Script for Atomic Check-and-Set (CAS) Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md This Lua script atomically checks if the versions of read documents match the versions recorded during the read phase. If they match, it proceeds to apply the writes and increments the versions. ```lua -- Check phase: verify all read versions match current for i = 1, #KEYS do local current_version = redis.call('JSON.GET', KEYS[i], '$._version') current_version = cjson.decode(current_version)[1] if current_version ~= tonumber(ARGV[i]) then return redis.error_reply("VERSION_CONFLICT") end end -- Write phase: only reached if all versions matched -- Apply all writes + increment versions for i = 1, #KEYS do local write_data = cjson.decode(ARGV[#KEYS + i]) write_data._version = tonumber(ARGV[i]) + 1 redis.call('JSON.MERGE', KEYS[i], '$', cjson.encode(write_data)) end return "OK" ``` -------------------------------- ### Buffered Write Command Descriptor Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Defines the structure for commands that are buffered during a transaction. These commands represent write operations that are not immediately executed. ```typescript type BufferedWrite = | { cmd: "json.set"; key: string; value: any } | { cmd: "json.merge"; key: string; value: any } | { cmd: "del"; key: string } ``` -------------------------------- ### Concave React Client Integration Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Provides the React client-side components for the todo app, using Concave's React hooks for queries and mutations within a provider. ```tsx "use client"; import { ConcaveProvider, useQuery, useMutation } from "concave/react"; function TaskApp() { const tasks = useQuery({ name: "tasks.list", _type: "query" }, {}); const searchResults = useQuery({ name: "tasks.search", _type: "query" }, { term: searchTerm }); const create = useMutation({ name: "tasks.create", _type: "mutation" }); const toggle = useMutation({ name: "tasks.toggle", _type: "mutation" }); const remove = useMutation({ name: "tasks.remove", _type: "mutation" }); // ... render UI with tasks, search bar, add/toggle/delete buttons } export default function Page() { return ( ); } ``` -------------------------------- ### Committing Transactions with Optimistic CAS Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md The commit process builds and executes a Lua script to atomically verify all read versions against current versions and then apply writes, incrementing the versions. It catches `VERSION_CONFLICT` errors. ```typescript async _commit() { // Build a Lua script that checks ALL versions atomically const script = buildOptimisticCASScript( this.writeBuffer, this.versionTracker ); try { await redis.eval(script, keys, args); } catch (error) { if (error.message.includes("VERSION_CONFLICT")) { throw new VersionConflictError(); } throw error; } } ``` -------------------------------- ### TypeScript Wrapper for Optimistic CAS Script Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md This TypeScript function provides a wrapper for executing the optimistic CAS Lua script. It prepares the keys and arguments, including versions and write data, and handles potential version conflicts by parsing the error message and throwing a specific `VersionConflictError`. This function is used to integrate the Lua script's functionality into a Node.js application. ```typescript async function commitWithOptimisticCAS( redis: Redis, writeBuffer: BufferedWrite[], versionTracker: Map ) { const keys = Array.from(versionTracker.keys()); const versions = keys.map(key => versionTracker.get(key)!); const writeDatas = keys.map(key => { const write = writeBuffer.find(w => w.key === key); return JSON.stringify(write?.data ?? {}); }); const args = [...versions, ...writeDatas]; try { await redis.eval(LUA_CAS_SCRIPT, keys.length, ...keys, ...args); } catch (error: any) { if (error.message?.includes('VERSION_CONFLICT')) { const match = error.message.match(/VERSION_CONFLICT:(.+?):expected=(\d+):actual=(\d+)/); throw new VersionConflictError({ key: match?.[1], expected: parseInt(match?.[2] ?? '0'), actual: parseInt(match?.[3] ?? '0'), }); } throw error; } } ``` -------------------------------- ### Patch Operation Implementation Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Implements the patch operation for the mutation context. It retrieves the current document, merges the fields, buffers the merge command, and updates the in-memory cache. Throws an error if the document is not found. ```typescript async patch(tableName: string, id: string, fields: Record): Promise { const key = `${tableName}:${id}`; const current = await this.get(tableName, id); if (!current) throw new ConcaveError(`Document ${key} not found`); const updated = { ...current, ...fields }; this.writeBuffer.push({ cmd: "json.merge", key, value: fields }); this.cache.set(key, updated); this.touchedTables.add(tableName); } ``` -------------------------------- ### ConcaveProvider Component Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Provides the server URL to hooks via React Context and manages SSE connection lifecycle and local query cache. ```tsx export function ConcaveProvider({ url, // e.g., "/api/concave" children, }: { url: string; children: React.ReactNode; }) { // Provides the server URL to all hooks via React Context // Manages the SSE connection lifecycle // Maintains a local query cache } ``` -------------------------------- ### Optimistic CAS Lua Script for Redis Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md This Lua script implements optimistic concurrency control for Redis JSON documents. It first checks if the versions of the specified keys match the expected versions provided in the arguments. If all versions match, it atomically applies all writes and increments the version number for each document. This script is crucial for handling concurrent updates without explicit locking. ```lua -- ARGV format: -- [version1, version2, ..., writeData1, writeData2, ...] -- KEYS format: -- [key1, key2, ...] local num_keys = #KEYS -- PHASE 1: Check all versions match for i = 1, num_keys do local key = KEYS[i] local expected_version = tonumber(ARGV[i]) -- Get current version from Redis local current_data = redis.call('JSON.GET', key, '$._version') -- Handle case where document doesn't exist if current_data == false or current_data == nil then return redis.error_reply('DOCUMENT_NOT_FOUND:' .. key) end local current_version = cjson.decode(current_data)[1] -- Version mismatch = conflict if current_version ~= expected_version then return redis.error_reply('VERSION_CONFLICT:' .. key .. ':expected=' .. expected_version .. ':actual=' .. current_version) end end -- PHASE 2: All versions matched! Apply all writes atomically for i = 1, num_keys do local key = KEYS[i] local expected_version = tonumber(ARGV[i]) local write_data = cjson.decode(ARGV[num_keys + i]) -- Increment version write_data._version = expected_version + 1 -- Merge the write (includes version increment) redis.call('JSON.MERGE', key, '$', cjson.encode(write_data)) end return 'OK' ``` -------------------------------- ### Tracking Document Versions on Read Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md When reading a document, its version is tracked to be used later for conflict detection during commit. This function is part of the mutation context. ```typescript async get(tableName: string, id: string) { const key = `${tableName}:${id}`; const doc = await redis.json.get(key, "$"); const result = doc?.[0]; // Remember this document's version this.versionTracker.set(key, result._version); return result; } ``` -------------------------------- ### Todo App Schema Definition Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Defines the schema for the tasks table in the todo app, including various field types like string, boolean, and number. ```typescript import { defineSchema, defineTable, s } from "concave"; export default defineSchema({ tasks: defineTable({ title: s.string(), description: s.string(), completed: s.boolean(), priority: s.number(), assignee: s.string().noStem(), }), }); ``` -------------------------------- ### Mutation Wrapper Function Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md A higher-order function that wraps a mutation handler, providing a buffered transaction mode by default. It manages context creation, commit, rollback, and emits invalidation events. ```typescript export function mutation({ args, handler, transactionMode = "buffer" }) { return { _type: "mutation" as const, _args: args, handler: async (rawCtx, validatedArgs: TArgs) => { const ctx = createMutationContext(rawCtx.redis, rawCtx.schema); try { const result = await handler(ctx, validatedArgs); await ctx._commit(); // Emit invalidation for each touched table for (const table of ctx._touchedTables) { await rawCtx.realtime.channel(`concave:${table}`).emit("invalidate", { ts: Date.now(), }); } return result; } catch (error) { ctx._rollback(); throw error; } }, }; } ``` -------------------------------- ### useMutation Hook for Data Modification Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Handles mutations by sending POST requests to the server and managing loading states. Throws a ConcaveError for non-ok responses. ```typescript export function useMutation( mutationRef: { _type: "mutation"; name: string } ) { const { url } = useConcaveContext(); const [isLoading, setIsLoading] = useState(false); const mutate = async (args: Record) => { setIsLoading(true); try { const res = await fetch(`${url}/mutate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ fn: mutationRef.name, args }), }); const result = await res.json(); if (!res.ok) throw new ConcaveError(result.error); return result.data; } finally { setIsLoading(false); } }; return Object.assign(mutate, { isLoading }); } ``` -------------------------------- ### Mutation Wrapper with Auto-Retry Logic Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md This wrapper function executes a mutation handler, automatically retrying the operation up to a specified number of times if a `VersionConflictError` occurs. It ensures fresh reads on each retry. ```typescript export function mutation({ handler, maxRetries = 3 }) { return { handler: async (rawCtx, args) => { for (let attempt = 0; attempt < maxRetries; attempt++) { const ctx = createMutationContext(rawCtx.redis, rawCtx.schema); try { const result = await handler(ctx, args); await ctx._commit(); // might throw VersionConflictError await emitInvalidation(rawCtx.realtime, ctx._touchedTables); return result; } catch (error) { ctx._rollback(); if (error instanceof VersionConflictError && attempt < maxRetries - 1) { // Retry with fresh reads continue; } throw error; // give up or non-conflict error } } } }; } ``` -------------------------------- ### Schema Definition Function Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Defines the overall schema by accepting a record of table definitions. It handles validation of table names and provides access to tables. ```typescript export function defineSchema(tables: Record) { return new SchemaDefinition(tables); } ``` -------------------------------- ### Commit Operation for Buffered Writes Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Executes all buffered write operations atomically using a Redis pipeline. It iterates through the write buffer and adds corresponding commands to the pipeline before execution. ```typescript async _commit(): Promise { if (this.writeBuffer.length === 0) return; const pipeline = redis.pipeline(); for (const op of this.writeBuffer) { switch (op.cmd) { case "json.set": pipeline.json.set(op.key, "$", op.value); break; case "json.merge": pipeline.json.merge(op.key, "$", op.value); break; case "del": pipeline.del(op.key); break; } } await pipeline.exec(); } ``` -------------------------------- ### Define Schema for Tasks Source: https://github.com/fahreddinozcan/concavex/blob/main/README.md Define the data schema for the 'tasks' table, specifying fields like title, completed status, and priority. ```typescript // concave/schema.ts import { defineSchema, defineTable, s } from "./src"; export default defineSchema({ tasks: defineTable({ title: s.string(), completed: s.boolean(), priority: s.number("U64"), }), }); ``` -------------------------------- ### Field Builders for Schema Definition Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Defines builders for various field types (string, number, boolean, id) used in schema definitions. Each field type supports specific configuration methods. ```typescript export const s = { string: () => new StringFieldDef(), number: () => new NumberFieldDef(), boolean: () => new BooleanFieldDef(), id: (table: string) => new IdFieldDef(table), }; ``` -------------------------------- ### Table Definition Function Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md A function to define a table schema, accepting a record of field definitions. It supports marking fields for full-text search and compiling the schema. ```typescript export function defineTable(fields: Record) { return new TableDefinition(fields); } ``` -------------------------------- ### Realtime Event Schema for Invalidation Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Defines the schema for the 'concave:invalidate' event using Zod. Specifies the structure for table, operations, and timestamp. ```typescript const realtimeSchema = { "concave:invalidate": z.object({ table: z.string(), ops: z.array(z.object({ type: z.enum(["insert", "update", "delete"]), table: z.string(), id: z.string(), })), ts: z.number(), }), }; ``` -------------------------------- ### Emit Invalidation Events Source: https://github.com/fahreddinozcan/concavex/blob/main/PLAN.md Emits invalidation events for each touched table after a mutation commit. Filters operations to include only those relevant to the specific table. ```typescript async function emitInvalidation( realtime: Realtime, touchedTables: Set, ops: Operation[] ) { for (const table of touchedTables) { await realtime.channel(`concave:${table}`).emit("invalidate", { table, ops: ops.filter(op => op.table === table), ts: Date.now(), }); } } ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.