mirror of
https://github.com/hanzo-apps/dataroom.git
synced 2026-10-30 14:47:36 +00:00
The client was already @hanzo/kv; only the vocabulary was still Redis. Every module, symbol and comment now says what it connects to: lib/redis.ts -> lib/kv.ts (exports kv, lockKv) lib/redis-job-store.ts -> lib/kv-job-store.ts (KvJobStore) lib/redis-download-job-store.ts -> lib/kv-download-job-store.ts lib/files/tus-redis-locker.ts -> lib/files/tus-kv-locker.ts (KvLocker, KvLock) lib/api/domains/redis.ts -> lib/api/domains/kv.ts (getKey) lib/redis/ -> lib/kv-queue/ KV_URL is now the only endpoint variable — the REDIS_URL fallback and the redis://localhost default are gone, and the default scheme is kv://, which @hanzo/kv parses natively (utils/index.js). .env.example gained the KV_URL it never documented, and lost UPSTASH_REDIS_REST_LOCKER_URL/TOKEN: nothing has read those since the client changed — the locker opens a second connection to the same KV, so there is no second endpoint to configure. The second connection is `lockKv`, not `locker`: both tus routes already have a local `const locker = new KvLocker(...)`, so naming the client `locker` made the lock take itself as its own client. Verified against the repo's own gate (npx tsc --noEmit): 100 errors before and after, 0 introduced, compared set-wise by file:line:code. Behaviour is untouched — this is naming, one connection string, and dead config. Co-authored-by: Hanzo Dev <[email protected]>
132 lines
3.9 KiB
TypeScript
132 lines
3.9 KiB
TypeScript
import { ERRORS, Lock, Locker, RequestRelease } from "@tus/utils";
|
|
import type KV from "@hanzo/kv";
|
|
|
|
/**
|
|
* KvLocker is an implementation of the Locker interface that manages locks in key-value store using KV.
|
|
* This class is designed for exclusive access control over resources, often used in scenarios like upload management.
|
|
*
|
|
* Key Features:
|
|
* - Ensures exclusive resource access by using a KV-based map to track locks.
|
|
* - Implements timeout for lock acquisition, mitigating deadlock situations.
|
|
* - Facilitates both immediate and graceful release of locks through different mechanisms.
|
|
*
|
|
* Locking Behavior:
|
|
* - When the `lock` method is invoked for an already locked resource, the `cancelReq` callback is called.
|
|
* This signals to the current lock holder that another process is requesting the lock, encouraging them to release it as soon as possible.
|
|
* - The lock attempt continues until the specified timeout is reached. If the timeout expires and the lock is still not
|
|
* available, an error is thrown to indicate lock acquisition failure.
|
|
*
|
|
* Lock Acquisition and Release:
|
|
* - The `lock` method implements a wait mechanism, allowing a lock request to either succeed when the lock becomes available,
|
|
* or fail after the timeout period.
|
|
* - The `unlock` method releases a lock, making the resource available for other requests.
|
|
*/
|
|
|
|
interface KvLockerOptions {
|
|
acquireLockTimeout?: number;
|
|
client: KV;
|
|
}
|
|
|
|
export class KvLocker implements Locker {
|
|
timeout: number;
|
|
client: KV;
|
|
|
|
constructor(options: KvLockerOptions) {
|
|
this.timeout = options.acquireLockTimeout ?? 1000 * 30; // default: 30 seconds
|
|
this.client = options.client;
|
|
}
|
|
|
|
newLock(id: string) {
|
|
return new KvLock(id, this, this.timeout);
|
|
}
|
|
}
|
|
|
|
class KvLock implements Lock {
|
|
constructor(
|
|
private id: string,
|
|
private locker: KvLocker,
|
|
private timeout: number = 1000 * 30, // default: 30 seconds
|
|
) {}
|
|
|
|
async lock(
|
|
signal: AbortSignal,
|
|
requestRelease: RequestRelease,
|
|
): Promise<void> {
|
|
const abortController = new AbortController();
|
|
const lock = await Promise.race([
|
|
this.waitTimeout(signal),
|
|
this.acquireLock(this.id, requestRelease, signal),
|
|
]);
|
|
|
|
abortController.abort();
|
|
|
|
if (!lock) {
|
|
throw ERRORS.ERR_LOCK_TIMEOUT;
|
|
}
|
|
}
|
|
|
|
protected async acquireLock(
|
|
id: string,
|
|
requestRelease: RequestRelease,
|
|
signal: AbortSignal,
|
|
): Promise<boolean> {
|
|
if (signal.aborted) {
|
|
return false;
|
|
}
|
|
|
|
const lockKey = `tus-lock-${id}`;
|
|
// SET key value NX PX timeout
|
|
const lock = await this.locker.client.set(lockKey, "locked", "PX", this.timeout, "NX");
|
|
|
|
if (lock === "OK") {
|
|
// Register a release request flag in KV
|
|
await this.locker.client.set(`requestRelease:${lockKey}`, "true", "PX", this.timeout);
|
|
return true;
|
|
}
|
|
|
|
// Check if the release was requested
|
|
const releaseRequestStr = await this.locker.client.get(
|
|
`requestRelease:${lockKey}`,
|
|
);
|
|
if (releaseRequestStr === "true") {
|
|
await requestRelease?.();
|
|
}
|
|
|
|
await new Promise((resolve, reject) => {
|
|
setImmediate(() => {
|
|
this.acquireLock(id, requestRelease, signal)
|
|
.then(resolve)
|
|
.catch(reject);
|
|
});
|
|
});
|
|
|
|
return false;
|
|
}
|
|
|
|
async unlock(): Promise<void> {
|
|
const lockKey = `tus-lock-${this.id}`;
|
|
const lockExists = await this.locker.client.del(lockKey);
|
|
if (!lockExists) {
|
|
throw new Error("Releasing an unlocked lock!");
|
|
}
|
|
|
|
// Clean up the request release entry
|
|
await this.locker.client.del(`requestRelease:${lockKey}`);
|
|
}
|
|
|
|
protected waitTimeout(signal: AbortSignal) {
|
|
return new Promise<boolean>((resolve) => {
|
|
const timeout = setTimeout(() => {
|
|
resolve(false);
|
|
}, this.timeout);
|
|
|
|
const abortListener = () => {
|
|
clearTimeout(timeout);
|
|
signal.removeEventListener("abort", abortListener);
|
|
resolve(false);
|
|
};
|
|
signal.addEventListener("abort", abortListener);
|
|
});
|
|
}
|
|
}
|