open-index/open-npm huggingface dataset:
npm package: @prsm/lock
Description: Distributed locking primitives for Redis - mutex and semaphore
Latest version: 1.0.2

<p align="center">
  <img src=".github/logo.svg" width="80" height="80" alt="lock logo">
</p>

<h1 align="center">@prsm/lock</h1>

Distributed locking primitives for Redis. Mutex (exclusive lock) and semaphore (N concurrent leases). Atomic, safe, crash-tolerant.

## Installation

```bash
npm install @prsm/lock
```

## Mutex

Exclusive lock. One holder at a time.

```js
import { mutex } from "@prsm/lock"

const lock = mutex({
  redis: { host: "127.0.0.1", port: 6379 },
})

const { acquired, id } = await lock.acquire("my-job", { ttl: "30s" })
if (!acquired) return

try {
  await doWork()
} finally {
  await lock.release("my-job", id)
}
```

### `mutex(options)`

- `redis` - `{ url?, host?, port?, password? }` passed to node-redis
- `prefix` - Redis key prefix (default `"lock:mutex:"`)

### Methods

| Method | Returns | Description |
|---|---|---|
| `acquire(key, opts?)` | `{ acquired, id }` | Attempt to acquire. `opts.ttl` (default `"10s"`), `opts.id` (custom holder ID) |
| `release(key, id)` | `boolean` | Release only if you still own it |
| `peek(key)` | `{ held, holder, ttl }` | Check lock state without acquiring |
| `close()` | `void` | Disconnect Redis |

## Semaphore

Up to N concurrent holders.

```js
import { semaphore } from "@prsm/lock"

const sem = semaphore({
  max: 20,
  ttl: "60s",
  redis: { host: "127.0.0.1", port: 6379 },
})

const { acquired, id } = await sem.acquire("worker-slots")
if (!acquired) return

const heartbeat = setInterval(() => sem.renew("worker-slots", id), 15000)

try {
  await processTask()
} finally {
  clearInterval(heartbeat)
  await sem.release("worker-slots", id)
}
```

### `semaphore(options)`

- `max` - Maximum concurrent holders (required)
- `ttl` - Lease lifetime (default `"60s"`). Expired leases are pruned automatically
- `redis` - Same as mutex
- `prefix` - Redis key prefix (default `"lock:sem:"`)

### Methods

| Method | Returns | Description |
|---|---|---|
| `acquire(key, opts?)` | `{ acquired, id }` | Acquire a lease slot. `opts.id` for custom lease ID |
| `release(key, id)` | `true` | Release a lease |
| `renew(key, id)` | `boolean` | Extend lease lifetime. `false` if expired |
| `count(key)` | `number` | Active lease count (after pruning) |
| `peek(key)` | `{ active, max, available, holders }` | Full semaphore state |
| `close()` | `void` | Disconnect Redis |

## How It Works

**Mutex** uses `SET key value NX PX ttl` for atomic acquire and a Lua script for ownership-checked release (only the holder can release).

**Semaphore** uses a Redis sorted set where each member is a lease ID and the score is a timestamp. Expired entries are pruned via `ZREMRANGEBYSCORE` before each acquire. Renewal updates the timestamp to extend the lease window.

Both are crash-tolerant via TTLs. If a holder dies without releasing, the lock/lease expires automatically.
npm package: @prsm/log
Description: Structured logging with child contexts
Author: nvms
Latest version: 1.0.2

<p align="center">
  <img src=".github/logo.svg" width="80" height="80" alt="log logo">
</p>

<h1 align="center">@prsm/log</h1>

Structured logging with child contexts. JSON in production, pretty in dev.

## Installation

```bash
npm install @prsm/log
```

## Usage

```js
import log from '@prsm/log'

log.info('server started', { port: 8080 })
log.warn('slow query', { ms: 1200 })
log.error(new Error('connection refused'))
```

Pretty output (dev):

```
[14:32:01] INFO  server started
    port: 8080
[14:32:01] WARN  slow query
    ms: 1200
[14:32:01] ERROR connection refused
    err: connection refused
```

JSON output (production):

```json
{"level":"info","msg":"server started","port":8080,"ts":1710782521000}
```

## Child Contexts

Create loggers that carry context through a request, job, or connection.

```js
const conn = log.child({ connId: 'abc-123' })
conn.info('command received', { cmd: 'submit' })
// [14:32:01] INFO  command received
//     connId: abc-123
//     cmd: submit

const db = conn.child({ db: 'postgres' })
db.info('query', { ms: 12 })
// [14:32:01] INFO  query
//     connId: c1
//     db: postgres
//     ms: 12
```

Context accumulates. Every log from a child includes all parent context.

## Configuration

```js
log.configure({
  level: 'debug',  // debug | info | warn | error | none
  pretty: false,   // true for human-readable, false for JSON
})
```

Defaults: level `info`, pretty when `NODE_ENV !== 'production'`.

## Tracing Across Subsystems

Pass context through queue jobs, realtime commands, workflows.

```js
server.exposeCommand('submit', async (ctx) => {
  const l = log.child({ connId: ctx.connection.id })
  l.info('received')
  await queue.push({ ...ctx.payload, _trace: l.context })
})

queue.process(async (payload, task) => {
  const l = log.child({ taskId: task.uuid, ...payload._trace })
  l.info('processing')
  // every log line carries connId + taskId
})
```

## License

MIT