Skip to main content

SharedLock middlewares

Initial configuration​

To begin using the shared-lock middlewares, you'll need to create and configure a SharedLockFactoryResolver instance:

./samples/shared-lock.ts
import { SharedLockFactoryResolver } from "eridu-tech/shared-lock";
import { MemorySharedLockAdapter } from "eridu-tech/shared-lock/memory-shared-lock-adapter";
import { RedisSharedLockAdapter } from "eridu-tech/shared-lock/redis-shared-lock-adapter";
import { Redis } from "ioredis";

export const sharedLockFactoryResolver = new SharedLockFactoryResolver({
adapters: {
memory: new MemorySharedLockAdapter(),
redis: new RedisSharedLockAdapter(
new Redis("YOUR_REDIS_CONNECTION_STRING"),
),
},
defaultAdapter: "memory",
});

await sharedLockFactoryResolver.init();

withSharedLockFactory middleware​

The SharedLock middleware wraps function calls with a distributed shared lock (reader-writer lock), providing concurrency control with two access modes: Reader mode ("READER") allows multiple callers to execute the wrapped function concurrently as long as no writer holds the lock, while Writer mode ("WRITER") grants exclusive access. Before executing the wrapped function, the appropriate lock is acquired on a key derived from the function's arguments.

Usage​

./samples/with-shared-lock.ts
import {
withSharedLockFactory,
SHARED_LOCK_WHEN,
} from "eridu-tech/shared-lock/middlewares";
import { use } from "eridu-tech/middleware";
import { sharedLockFactoryResolver } from "./shared-lock.js";

const withSharedLock = withSharedLockFactory(sharedLockFactoryResolver);

const readData = async (key: string): Promise<unknown> => {
// Safe to run concurrently with other readers
return { data: "..." };
};

// Wrap with shared-lock in reader mode using the default adapter (`memory`) — multiple readers allowed
const safeRead = use(
readData,
withSharedLock({
key: ([resourceKey]) => `data:${resourceKey}`,
when: SHARED_LOCK_WHEN.READER,
limit: 10, // Up to 10 concurrent readers
}),
);

// Wrap with shared-lock in reader mode using a specific adapter (`redis`)
const safeReadOnRedis = use(
readData,
withSharedLock.use("redis")({
key: ([resourceKey]) => `data:${resourceKey}`,
when: SHARED_LOCK_WHEN.READER,
limit: 10,
}),
);

const writeData = async (key: string): Promise<void> => {
// Safe to run concurrently as only writer
};

// Wrap with shared-lock in writer mode using the default adapter (`memory`) — only writer allowed
const safeWrite = use(
writeData,
withSharedLock({
key: ([resourceKey]) => `data:${resourceKey}`,
when: SHARED_LOCK_WHEN.WRITER,
limit: 10,
}),
);

// Wrap with shared-lock in writer mode using a specific adapter (`redis`)
const safeWriteOnRedis = use(
writeData,
withSharedLock.use("redis")({
key: ([resourceKey]) => `data:${resourceKey}`,
when: SHARED_LOCK_WHEN.WRITER,
limit: 10,
}),
);

await safeReadOnRedis("config");
await safeWrite("config");
await safeWriteOnRedis("config");

Calling withSharedLock() directly uses the resolver's default adapter (memory). Use withSharedLock.use("redis") to run the wrapped function through a specific registered adapter.

info

Here is a complete list of settings for the withSharedLock function.

Settings​

OptionTypeDescription
keyInvocable<TParameters, string>A function that produces the lock key from the wrapped function's arguments. All consumers using the same key share the same lock state
whenSharedLockWhenSettingWhether to acquire the lock in "READER" or "WRITER" mode
limitnumberMaximum number of concurrent readers allowed when the lock is acquired in reader mode
lockIdInvocable<TParameters, string>Optional function that produces a unique identifier for the current lock acquisition attempt. Defaults to a UUID (v4)
ttlITimeSpan | nullTime-to-live for the lock. null means the lock never expires automatically; if omitted the factory's default TTL is used

withHttpSharedLockError middleware​

The withHttpSharedLockError middleware converts every shared-lock error (reader semaphore, writer lock and shared-lock errors) thrown by the wrapped endpoint or downstream middleware into an HttpError, so the request fails with a meaningful status code instead of a generic 500. Errors that are not shared-lock errors are rethrown untouched, and the original error is preserved as the cause.

Usage​

./samples/with-http-shared-lock-error.ts
import { HttpRouter, defaultHttpRouterAdapter } from "eridu-tech/http-router";
import { LimitReachedReaderSemaphoreError } from "eridu-tech/shared-lock/contracts";
import { withHttpSharedLockError } from "eridu-tech/shared-lock/middlewares";

const router = new HttpRouter({
router: defaultHttpRouterAdapter(),
});

router.use(withHttpSharedLockError());

router.endpoint({
url: "/catalog/1",
method: ["POST"],
handler: async () => {
// A 429 response when no reader slot is available
throw LimitReachedReaderSemaphoreError.create("catalog");
},
});

Status codes​

ErrorsStatus
LimitReachedReaderSemaphoreError429
FailedRefreshReaderSemaphoreError, FailedReleaseReaderSemaphoreError, FailedAcquireWriterLockError, FailedRefreshWriterLockError, FailedReleaseWriterLockError409
UnableToAcquireReaderSemaphoreError, UnableToReleaseReaderSemaphoreError, UnableToRefreshReaderSemaphoreError, UnableToAcquireWriterLockError, UnableToReleaseWriterLockError, UnableToRefreshWriterLockError, UnableToForceReleaseSharedLockError, UnableToGetStateSharedLockError503

A 429 response means the reader semaphore has no free slot left. A 409 response means the caller's slot or lock is no longer valid or is owned by another caller. A 503 response means the shared-lock adapter failed, so the same request can be retried later. The original error is preserved as the cause, and no identifiers are exposed in the response body.

Further information​

For further information refer to eridu-tech/shared-lock API docs.