Skip to main content

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");
danger

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 query
  • node_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 query
  • node_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 into
  • data: 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 node
  • node_id: The node to update
  • field: 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" });
caution

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 node
  • node_id: The node to update
  • field: 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 node
  • node_id: The node to update
  • field: Array field name
  • value: 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 node
  • node_id: The node to update
  • field: Array field name
  • value: 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 node
  • node_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)
note

Deleted nodes' disk space is not immediately reclaimed. The data remains in .dat files but becomes inaccessible.

Transaction Patterns

// 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

  1. Keep transactions short: Each operation executes immediately
  2. Handle failures explicitly: No automatic rollback
  3. Avoid nested fields: Use flat structures for better performance
  4. Batch reads with mget: More efficient than multiple get() calls
  5. Use typed stores: Leverage TypeScript for compile-time safety
  6. Validate data size: Keep documents under 25KB
  7. Index strategically: Plan indexes before inserting data