Skip to Content
Jumentix DocsPackages@jumentix/mutex-servicemutex-service usage

@jumentix/mutex-service — usage guide

Responsibility in context

  • Stack layer: persistence / coordination adapter
  • Owns: named distributed/process locks backed by a KV client
  • Used with: @jumentix/key-value-storage
  • Not responsible for: storing business documents or messaging

What it is

@jumentix/mutex-service provides named locks backed by @jumentix/key-value-storage. Two workers (or two requests) cannot hold the same lock id for the same resource name at once. The service exposes lock, isLocked, and unlock — all returning ServiceResponse.

Why it exists

Without a shared lock, duplicate webhooks, cron overlap, or parallel tab writes can corrupt the same invoice, seat, or design record. Junior teams need a small, testable lock API that works in memory during development and on Redis in production — using the same KV port the rest of the stack already uses.

Prerequisites

  • Packages: @jumentix/mutex-service and @jumentix/key-value-storage.
  • Runtime: browser (InMemory KV + playground stub) or Node/Bun (InMemory or Redis KV).
  • Prior reading: key-value-storage usage.
  • Concept: a lock key is {mutexPrefix}:{resourceName}:{uuid} where uuid identifies this lock attempt (use crypto.randomUUID()).

Glossary

TermMeaning
Resource nameLogical name of what you protect (e.g. invoice-42, design-sync).
Lock uuidUnique id for one lock attempt; stored as the KV value marker.
Previously lockedlock() result when another holder already owns the resource.
PrefixNamespace for mutex keys; default mutex: (configurable via IMutexServiceOptions).
Critical sectionCode between successful lock and unlock — keep it short.
ServiceResponse{ result?, error? } — inspect error before trusting result.

Steps

1. Install (< 5 minutes)

bun add @jumentix/mutex-service @jumentix/key-value-storage

2. First success — acquire and release (< 15 minutes)

import { MutexService } from '@jumentix/mutex-service';
import { InMemoryKeyValueStorageClient } from '@jumentix/key-value-storage';

const kv = InMemoryKeyValueStorageClient.compile();
await kv.connect();

const mutex = MutexService.compile(kv);
const lockId = crypto.randomUUID();
const resource = 'docs-demo';

const acquired = await mutex.lock(resource, lockId);
if (acquired.error) throw acquired.error;

if (!acquired.result?.locked) {
  console.log('Resource busy', acquired.result?.previouslyLocked);
} else {
  try {
    // critical section — keep tiny
    await doWork(resource);
  } finally {
    await mutex.unlock(resource, lockId);
  }
}

Verify success: first lock returns { locked: true }; after unlock, isLocked returns { result: false }.

3. Core workflow — always release in finally

async function withLock<T>(
  mutex: MutexService,
  resource: string,
  fn: () => Promise<T>
): Promise<T | null> {
  const lockId = crypto.randomUUID();
  const { result, error } = await mutex.lock(resource, lockId);
  if (error) throw error;
  if (!result?.locked) return null;

  try {
    return await fn();
  } finally {
    await mutex.unlock(resource, lockId);
  }
}

Never hold a lock across slow remote I/O unless you accept timeout and stale-lock risk.

4. Core workflow — check before retry

const status = await mutex.isLocked(resource, lockId);
if (status.result) {
  // still held — back off or surface "busy" to the user
}

5. Core workflow — production wiring

import { compileKeyValueStorageClient } from '@jumentix/key-value-storage';
import { MutexService } from '@jumentix/mutex-service';

const kv = compileKeyValueStorageClient(); // Redis in production
await kv.connect();
const mutex = MutexService.compile(kv, { prefix: 'myapp-mutex' });

Use a dedicated prefix per service so lock keys never collide with cache keys.

6. Full surface — API map

MemberReturnsNotes
MutexService.compile(kv, options?)Singleton MutexServiceRequires connected KV client
lock(resourceName, uuid)ServiceResponse with { locked, previouslyLocked }Does not block-wait; returns busy state
isLocked(resourceName, uuid)ServiceResponse<boolean>Checks KV marker
unlock(resourceName, uuid)ServiceResponseIdempotent delete of lock key
MutexService.reset()voidTest helper — clears singleton

This package does not implement queueing or lease renewal — callers retry or back off when locked: false.

Try it in the docs playground

Mutex with in-memory KV

Protect a Category update while two Task writers compete for the same resource.

const keyValue = api.createKeyValueStorage();
const mutex = api.create(keyValue);

const firstWriter = await mutex.lock('category', 'work');
const secondWriter = await mutex.lock('category', 'work');
const lockedBeforeRelease = await mutex.isLocked('category', 'work');
await mutex.unlock('category', 'work');
const lockedAfterRelease = await mutex.isLocked('category', 'work');

return {
  firstWriter: firstWriter.result,
  secondWriter: secondWriter.result,
  lockedBeforeRelease: lockedBeforeRelease.result,
  lockedAfterRelease: lockedAfterRelease.result
};

The playground uses a simplified acquire/release API:

const mutex = api.create();
const lock = await mutex.acquire('docs-demo');
await mutex.release(lock);

Map that mentally to lock / unlock with a resource name and uuid in real code.

Common errors

SymptomCauseFixVerify success
locked: false every timeSame resource already lockedWait or use another resource name; call unlock in prior pathSecond attempt after unlock succeeds
Lock never releasedMissing finally / early returnWrap body in try/finally with unlockisLocked false after flow
Stale lock after crashProcess died before unlockTTL strategy outside this package or manual unlock with known uuidResource writable again
MutexService depends on KeyValueStorageClientPassed null KV clientCompile KV firstMutexService.compile(kv) succeeds
Tests interfereSingleton mutex + shared KVMutexService.reset() between testsIsolated lock outcomes per test

Junior checklist (“I can …”)

  • Wire MutexService.compile with an in-memory KV client.
  • Acquire a lock with a fresh uuid and release it in finally.
  • Explain the difference between locked: false and an error response.
  • Keep a critical section small and free of long await fetch chains.
  • Configure a custom prefix for production Redis.
  • Describe when not to use in-memory locks (multi-process workers).

Next step

Return to the shared storage port in key-value-storage, then learn decoupled messaging in message-mediator.