Skip to content
tRPC

Locks

experimental_lock() runs calls with the same key one at a time, across instances.

On this page

experimental_lock() runs the rest of the call under a lock, so calls with the same key never overlap: the second waits for the first, and fails with experimental_LockConflict (CONFLICT, 409) if timeout (10 seconds) passes first. The resolver gets ctx.lock.waited.

import { import NodeRedisNodeRedis } from '@effect/platform-node';
import {
  
function experimental_lock<TReq extends MiddlewareRequirements = {}>(options: experimental_LockMiddlewareOptions<ReqCtx<TReq>, ReqInput<TReq>, ReqMeta<TReq>>): EffectMiddleware<ReqCtx<TReq>, ReqInput<TReq>, ReqMeta<TReq>, {
    readonly lock: experimental_Lock;
}, experimental_LockConflict, experimental_Locker, never, never, Unset>
experimental_lock
,
function experimental_redisLocker(options?: experimental_RedisLockerOptions): Layer.Layer<experimental_Locker, never, Redis>experimental_redisLocker, } from '@trpc12/trpc/middleware'; const
const pay: BuildProcedure<With<InputDef<RootDef<{}, {}>, z.ZodObject<{
    invoice: z.ZodString;
}, z.core.$strip>>, {
    ctx: {};
    ctxAdded: {
        readonly lock: experimental_Lock;
    };
    inputOut: {
        invoice: string;
    };
    errors: experimental_LockConflict | InputValidationError<{
        readonly code: "invalid_type";
        readonly expected: z.core.$ZodInvalidTypeExpected;
        readonly path: readonly [] | readonly ["invoice"];
        readonly message: string;
    } | {
        readonly code: "too_big";
        readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
        ... 4 more ...;
        readonly message: string;
    } | ... 9 more ... | {
        ...;
    }>;
    shorts: never;
    provided: Response;
    requires: experimental_Locker;
}>, "mutation", {
    ...;
}>
pay
= const t: TrpcRoot<{}, {}, RootDef<{}, {}>>t.TrpcRoot<{}, {}, RootDef<{}, {}>>.procedure: ProcedureBuilder<RootDef<{}, {}>>procedure
.
ProcedureBuilder<RootDef<{}, {}>>.input<z.ZodObject<{
    invoice: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    invoice: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<InputDef<RootDef<{}, {}>, z.ZodObject<{
    invoice: z.ZodString;
}, z.core.$strip>>> (+1 overload)
input
(import zz.
function object<{
    invoice: z.ZodString;
}>(shape?: {
    invoice: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    invoice: z.ZodString;
}, z.core.$strip>
object
({ invoice: z.ZodStringinvoice: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string() }))
.
ProcedureBuilder<InputDef<RootDef<{}, {}>, ZodObject<{ invoice: ZodString; }, $strip>>>.use<{
    readonly lock: experimental_Lock;
}, experimental_LockConflict, experimental_Locker, never, never, Unset, {}>(mw: EffectMiddleware<{}, {
    invoice: string;
}, {}, {
    readonly lock: experimental_Lock;
}, experimental_LockConflict, experimental_Locker, never, never, Unset>): ProcedureBuilder<With<InputDef<RootDef<{}, {}>, z.ZodObject<{
    invoice: z.ZodString;
}, z.core.$strip>>, {
    ...;
}>> (+1 overload)
use
(
experimental_lock<{
    input: {
        invoice: string;
    };
}>(options: experimental_LockMiddlewareOptions<{}, {
    invoice: string;
}, {}>): EffectMiddleware<{}, {
    invoice: string;
}, {}, {
    readonly lock: experimental_Lock;
}, experimental_LockConflict, experimental_Locker, never, never, Unset>
experimental_lock
<{
input: {
    invoice: string;
}
input
: { invoice: stringinvoice: string } }>({
experimental_LockMiddlewareOptions<{}, { invoice: string; }, {}>.key: (opts: {
    readonly ctx: {};
    readonly input: {
        invoice: string;
    };
    readonly meta: {};
    readonly path: string;
}) => string
key
: (
opts: {
    readonly ctx: {};
    readonly input: {
        invoice: string;
    };
    readonly meta: {};
    readonly path: string;
}
opts
) => `invoice:${
opts: {
    readonly ctx: {};
    readonly input: {
        invoice: string;
    };
    readonly meta: {};
    readonly path: string;
}
opts
.
input: {
    invoice: string;
}
input
.invoice: stringinvoice}`,
}), ) .
ProcedureBuilder<With<InputDef<RootDef<{}, {}>, ZodObject<{ invoice: ZodString; }, $strip>>, { ctx: {}; ctxAdded: { readonly lock: experimental_Lock; }; ... 4 more ...; requires: experimental_Locker; }>>.mutation<void>(resolver: (opts: ResolverOpts<With<InputDef<RootDef<{}, {}>, z.ZodObject<{
    invoice: z.ZodString;
}, z.core.$strip>>, {
    ctx: {};
    ctxAdded: {
        readonly lock: experimental_Lock;
    };
    inputOut: {
        invoice: string;
    };
    errors: experimental_LockConflict | InputValidationError<{
        readonly code: "invalid_type";
        readonly expected: z.core.$ZodInvalidTypeExpected;
        readonly path: readonly [] | readonly ["invoice"];
        readonly message: string;
    } | {
        readonly code: "too_big";
        readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
        ... 4 more ...;
        readonly message: string;
    } | ... 9 more ... | {
        ...;
    }>;
    shorts: never;
    provided: Response;
    requires: experimental_Locker;
}>>) => void): BuildProcedure<...>
mutation
((
opts: ResolverOpts<With<InputDef<RootDef<{}, {}>, z.ZodObject<{
    invoice: z.ZodString;
}, z.core.$strip>>, {
    ctx: {};
    ctxAdded: {
        readonly lock: experimental_Lock;
    };
    inputOut: {
        invoice: string;
    };
    errors: experimental_LockConflict | InputValidationError<{
        readonly code: "invalid_type";
        readonly expected: z.core.$ZodInvalidTypeExpected;
        readonly path: readonly [] | readonly ["invoice"];
        readonly message: string;
    } | {
        readonly code: "too_big";
        readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
        ... 4 more ...;
        readonly message: string;
    } | ... 9 more ... | {
        ...;
    }>;
    shorts: never;
    provided: Response;
    requires: experimental_Locker;
}>>
opts
) =>
function charge(invoice: string, opts: {
    retried: boolean;
}): void
charge
(
opts: ResolverOpts<With<InputDef<RootDef<{}, {}>, z.ZodObject<{
    invoice: z.ZodString;
}, z.core.$strip>>, {
    ctx: {};
    ctxAdded: {
        readonly lock: experimental_Lock;
    };
    inputOut: {
        invoice: string;
    };
    errors: experimental_LockConflict | InputValidationError<{
        readonly code: "invalid_type";
        readonly expected: z.core.$ZodInvalidTypeExpected;
        readonly path: readonly [] | readonly ["invoice"];
        readonly message: string;
    } | {
        readonly code: "too_big";
        readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
        ... 4 more ...;
        readonly message: string;
    } | ... 9 more ... | {
        ...;
    }>;
    shorts: never;
    provided: Response;
    requires: experimental_Locker;
}>>
opts
.
ResolverOpts<With<InputDef<RootDef<{}, {}>, ZodObject<{ invoice: ZodString; }, $strip>>, { ctx: {}; ctxAdded: { readonly lock: experimental_Lock; }; ... 4 more ...; requires: experimental_Locker; }>>.input: {
    invoice: string;
}
input
.invoice: stringinvoice, { retried: booleanretried:
opts: ResolverOpts<With<InputDef<RootDef<{}, {}>, z.ZodObject<{
    invoice: z.ZodString;
}, z.core.$strip>>, {
    ctx: {};
    ctxAdded: {
        readonly lock: experimental_Lock;
    };
    inputOut: {
        invoice: string;
    };
    errors: experimental_LockConflict | InputValidationError<{
        readonly code: "invalid_type";
        readonly expected: z.core.$ZodInvalidTypeExpected;
        readonly path: readonly [] | readonly ["invoice"];
        readonly message: string;
    } | {
        readonly code: "too_big";
        readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
        ... 4 more ...;
        readonly message: string;
    } | ... 9 more ... | {
        ...;
    }>;
    shorts: never;
    provided: Response;
    requires: experimental_Locker;
}>>
opts
.
ResolverOpts<With<InputDef<RootDef<{}, {}>, ZodObject<{ invoice: ZodString; }, $strip>>, { ctx: {}; ctxAdded: { readonly lock: experimental_Lock; }; ... 4 more ...; requires: experimental_Locker; }>>.ctx: {
    readonly lock: experimental_Lock;
}
ctx
.lock: experimental_Locklock.experimental_Lock.waited: booleanwaited }),
);
createFetchHandler<{
    pay: typeof pay;
}, undefined>(opts: FetchHandlerOptions<{
    pay: typeof pay;
}, undefined>): FetchHandler<undefined>
createFetchHandler
({
BaseHandlerOptions<{ pay: typeof pay; }>.router: {
    pay: typeof pay;
}
router
,
layer: Layer.Layer<experimental_Locker, any, never>layer: function experimental_redisLocker(options?: experimental_RedisLockerOptions): Layer.Layer<experimental_Locker, never, Redis>experimental_redisLocker().Pipeable.pipe<Layer.Layer<experimental_Locker, never, Redis>, Layer.Layer<experimental_Locker, RedisError, never>>(this: Layer.Layer<experimental_Locker, never, Redis>, ab: (_: Layer.Layer<experimental_Locker, never, Redis>) => Layer.Layer<experimental_Locker, RedisError, never>): Layer.Layer<experimental_Locker, RedisError, never> (+21 overloads)pipe( import LayerLayer.const provide: <never, RedisError, Redis | NodeRedis.NodeRedis>(that: Layer.Layer<Redis | NodeRedis.NodeRedis, RedisError, never>) => <RIn2, E2, ROut2>(self: Layer.Layer<ROut2, E2, RIn2>) => Layer.Layer<ROut2, RedisError | E2, Exclude<RIn2, Redis | NodeRedis.NodeRedis>> (+3 overloads)provide(import NodeRedisNodeRedis.const layer: (options?: RedisClientOptions<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping, RedisSocketOptions> | undefined) => Layer.Layer<Redis | NodeRedis.NodeRedis, RedisError>layer({ RedisClientOptions<M extends RedisModules = RedisModules, F extends RedisFunctions = RedisFunctions, S extends RedisScripts = RedisScripts, RESP extends RespVersions = 3, TYPE_MAPPING extends TypeMapping = TypeMapping, SocketOptions extends RedisSocketOptions = RedisSocketOptions>.url?: string | undefinedurl: var process: NodeJS.Processprocess.NodeJS.Process.env: NodeJS.ProcessEnvenv.string | undefinedREDIS_URL })), ), });

A holder loses the lock after ttl (30 seconds), so a crashed instance can't hold it forever. A key the call already holds, from an earlier experimental_lock() in its chain, isn't taken twice. The lock covers the resolver; a streamed output or a subscription keeps running after it's released.

Lockers

The locker is an Effect service, experimental_Locker, provided as a layer:

  • experimental_memoryLocker() for one process.
  • experimental_redisLocker() over Effect's Redis for many. Any Redis Effect's Redis.make({ send, subscribe }) can wrap works, Upstash's REST client included.
  • experimental_durableLocker() on Workers.

Outside a procedure, take a lock with the service: yield* (yield* experimental_Locker).acquire(key) holds it until the surrounding Effect.scoped ends.