Transactions
All data operations in Garak are performed within a transaction context. Transactions in Garak are not ACID transactions like in SQL databases. Instead, each operation executes immediately when called.
Creating a Transaction
const db = new Garak.DB(url, accessToken, "silo-name");
const tx = db.tx("shard-id");
Transactions execute operations immediately - they are not batched or rolled back as a unit.
Transactions, as a concept, are implemented purely in Garak clients to simpify tenancy. Garak internally does not have the concept of transactions.
Read Operations
get() - Retrieve a Single Node
const user = await tx.get(usersStore, nodeId);
Parameters:
store: The store to querynode_id: The node ID to retrieve
Returns: The document data or throws if not found
Example:
const usersStore = new Garak.Store<User>(db, "users");
const tx = db.tx("my-shard");
const user = await tx.get(usersStore, 12345);
console.log(user.email); // Access typed fields
mget() - Retrieve Multiple Nodes
const result = await tx.mget(usersStore, [123, 456, 789]);
Parameters:
store: The store to querynode_ids: Array of node IDs to retrieve
Returns:
{
nodes: Array<{
node_id: number;
value: T | null; // null if node not found
}>
}
Example:
const { nodes } = await tx.mget(usersStore, [100, 200, 300]);
for (const node of nodes) {
if (node.value !== null) {
console.log(`Node ${node.node_id}:`, node.value);
} else {
console.log(`Node ${node.node_id}: not found`);
}
}
Write Operations
insert() - Create a New Node
const { node_id } = await tx.insert(store, data);
Parameters:
store: The store to insert intodata: The document data (must be < 25KB serialized)
Returns: Object with the generated node_id
Example:
type User = {
email: string;
name: string;
createdAt: number;
};
const usersStore = new Garak.Store<User>(db, "users");
const tx = db.tx("my-shard");
const { node_id } = await tx.insert(usersStore, {
email: "alice@example.com",
name: "Alice",
createdAt: Date.now()
});
console.log(`Created user with ID: ${node_id}`);
Side Effects:
- Generates sequential node_id
- Updates all relevant indexes
- Writes to oplog for replication
- May fail if secondary key constraints are violated
set() - Update a Field Value
await tx.set(store, nodeId, "fieldName", newValue);
Parameters:
store: The store containing the nodenode_id: The node to updatefield: Field name (supports dot notation for nested fields)value: The new value
Returns: The updated document
Example:
// Simple field
await tx.set(usersStore, 12345, "name", "Bob");
// Nested field (not recommended)
await tx.set(usersStore, 12345, "profile.bio", "Software engineer");
// Replace entire object
await tx.set(usersStore, 12345, "settings", { theme: "dark", lang: "en" });
While dot notation is supported for backwards compatibility, nested fields have performance overhead. Prefer flat document structures.
inc() - Increment a Numeric Field
await tx.inc(store, nodeId, "fieldName", amount);
Parameters:
store: The store containing the nodenode_id: The node to updatefield: Field name (must be numeric)value: Amount to increment (can be negative)
Returns: The updated document
Example:
// Increment by 1
await tx.inc(usersStore, 12345, "loginCount", 1);
// Decrement by 1
await tx.inc(usersStore, 12345, "credits", -1);
// Initialize field to 0 if nullish, then increment
await tx.inc(usersStore, 12345, "viewCount", 5);
Behavior:
- If field is nullish, initializes to 0 before incrementing
- Field must be a number or null/undefined
- Throws error if field is non-numeric (KIND_OPERATION_INCOMPATIBLE_WITH_FIELD)
addToSet() - Add to Array (if not present)
await tx.addToSet(store, nodeId, "arrayField", value);
Parameters:
store: The store containing the nodenode_id: The node to updatefield: Array field namevalue: Value to add (if not already in array)
Returns: The updated document
Example:
type Team = {
members: string[];
tags: string[];
};
const teamsStore = new Garak.Store<Team>(db, "teams");
// Add member (idempotent)
await tx.addToSet(teamsStore, 100, "members", "user@example.com");
// Add multiple items (must call separately)
await tx.addToSet(teamsStore, 100, "tags", "engineering");
await tx.addToSet(teamsStore, 100, "tags", "product");
Behavior:
- If field is nullish, initializes as empty array []
- Only adds value if not already present (set semantics)
- Field must be an array or null/undefined
- Throws error if field is not an array (ETARGETFIELDNOTARR)
pull() - Remove from Array
await tx.pull(store, nodeId, "arrayField", value);
Parameters:
store: The store containing the nodenode_id: The node to updatefield: Array field namevalue: Value to remove
Returns: The updated document
Example:
// Remove a member
await tx.pull(teamsStore, 100, "members", "user@example.com");
// Remove all occurrences of value
await tx.pull(teamsStore, 100, "tags", "deprecated");
Behavior:
- Removes all occurrences of the value
- No-op if value not in array
- Field must be an array
delete() - Delete a Node
await tx.delete(store, nodeId);
Parameters:
store: The store containing the nodenode_id: The node to delete
Returns: Success confirmation
Example:
await tx.delete(usersStore, 12345);
Side Effects:
- Marks node as deleted (zero-length entry in index)
- Removes from all indexes
- Writes to oplog
- Index removal failures are ignored (best-effort deindexing)
Deleted nodes' disk space is not immediately reclaimed. The data remains in .dat files but becomes inaccessible.
Transaction Patterns
Create and Link Pattern
// Create main entity
const { node_id: teamId } = await tx.insert(teamsStore, {
name: "Engineering",
members: [],
createdAt: Date.now()
});
// Create related entity referencing the first
const { node_id: projectId } = await tx.insert(projectsStore, {
teamId: teamId,
name: "Project Alpha",
status: "active"
});
Update with Conditional Logic
// Read current state
const user = await tx.get(usersStore, userId);
// Apply business logic
if (user.credits > 0) {
await tx.inc(usersStore, userId, "credits", -1);
await tx.inc(usersStore, userId, "purchaseCount", 1);
}
Batch Operations
// Process multiple nodes
const nodeIds = [100, 200, 300];
for (const nodeId of nodeIds) {
await tx.set(usersStore, nodeId, "status", "active");
}
// Or use mget for batch reads
const { nodes } = await tx.mget(usersStore, nodeIds);
for (const node of nodes) {
if (node.value && node.value.status === "pending") {
await tx.set(usersStore, node.node_id, "status", "processed");
}
}
Working with Arrays
type Document = {
tags: string[];
collaborators: string[];
};
const docsStore = new Garak.Store<Document>(db, "documents");
// Build up an array
await tx.addToSet(docsStore, docId, "tags", "important");
await tx.addToSet(docsStore, docId, "tags", "reviewed");
await tx.addToSet(docsStore, docId, "tags", "urgent");
// Remove items
await tx.pull(docsStore, docId, "tags", "reviewed");
// Manage collaborators
await tx.addToSet(docsStore, docId, "collaborators", "alice@example.com");
await tx.addToSet(docsStore, docId, "collaborators", "bob@example.com");
Type Safety
Garak's TypeScript client provides full type safety:
type User = {
email: string;
name: string;
age: number;
tags?: string[];
};
const usersStore = new Garak.Store<User>(db, "users");
// TypeScript enforces document structure
const { node_id } = await tx.insert(usersStore, {
email: "test@example.com",
name: "Test User",
age: 25
}); // ✓ Valid
// TypeScript catches type errors
await tx.set(usersStore, node_id, "age", "thirty"); // ✗ Type error
// Field names are type-checked
await tx.inc(usersStore, node_id, "loginCount", 1); // ✗ loginCount not in User type
Error Handling
try {
await tx.insert(usersStore, userData);
} catch (error) {
if (error instanceof DatabaseError) {
console.error(`Database error: ${error.code}`);
console.error(`Field: ${error.field}`);
console.error(`Message: ${error.message}`);
console.error(`Kind: ${error.kind}`);
// Handle specific error kinds
if (error.kind === "KIND_SCHEMA_VIOLATION") {
// Handle uniqueness violation
} else if (error.kind === "KIND_OVERSIZE_NODE") {
// Handle document too large
}
}
}
Best Practices
- Keep transactions short: Each operation executes immediately
- Handle failures explicitly: No automatic rollback
- Avoid nested fields: Use flat structures for better performance
- Batch reads with mget: More efficient than multiple get() calls
- Use typed stores: Leverage TypeScript for compile-time safety
- Validate data size: Keep documents under 25KB
- Index strategically: Plan indexes before inserting data