Has anyone combined trpc with durable

Has anyone combined trpc with durable objects to create a nicer wrapper around the DO API?
14 Replies
Unknown User
Unknown User•2y ago
Message Not Public
Sign In & Join Server To View
Cole
Cole•2y ago
Yeah, that makes sensse. I'm about halfway through building something very similar to that, but with trpc in front. I'd love to get your review on the code if you'd like. I just don't understand DOs that well.
Unknown User
Unknown User•2y ago
Message Not Public
Sign In & Join Server To View
Cole
Cole•2y ago
I'm using trpc@10 as well. I didn't realize I could use their built-in fetch adaptor, though! That's good news
Unknown User
Unknown User•2y ago
Message Not Public
Sign In & Join Server To View
Cole
Cole•2y ago
I love this example. This is super helpful. Oh... I now see what you mean by fetch redirects...
Unknown User
Unknown User•2y ago
Message Not Public
Sign In & Join Server To View
Cole
Cole•2y ago
As in, httpLink cannot accept a custom fetch function
Unknown User
Unknown User•2y ago
Message Not Public
Sign In & Join Server To View
Cole
Cole•2y ago
Thanks for the guidance, @koudelkaa. I need to call it a night, here.But, I'll bring in your recommended executor tomorrow! I'm really excited about it.
Unknown User
Unknown User•2y ago
Message Not Public
Sign In & Join Server To View
Cole
Cole•2y ago
This isn't done, but it's a start: https://gist.github.com/colelawrence/ba912db3cd1539ee820c3fe5e67654df I need to actually incorporate your middlewares to catch the outbound fetch, I will also want to add a zod validation layer for the storage, but I have some snippets from other places that should make that easy.
Gist
trpc-durable-object.ts
GitHub Gist: instantly share code, notes, and snippets.
Cole
Cole•2y ago
I havd some other experiments with websockets earlier on, but I realized that I won't need websockets for a little while anyway. So, I can add those to this interface later on. Alright, ciao! 👋 Definitely some issues with my initial implementation's approach to creating context. I will have a better example to show later on after I've tried a few things Fixed it up and made some nice adjustments. It seems to be working for my use cases. Here's a not very good RateLimiter using trpc:
import { invariant, z } from "@autoplay/utils";
import type { Env } from "../Env";

import { initTRPCDurableObject } from "../helpers/createTrpcDurableObject";

export const tdo = initTRPCDurableObject<Env>();
export const { t } = tdo;

export const RateLimiter = tdo.object(
async (state, _env, check) => {
// Timestamp at which this IP will next be allowed to send a message. Start in the distant
// past, i.e. the IP can send a message now.
const nextAllowedTimeStored = await state.storage.get('nextAllowedTime') ?? 0
let nextAllowedTime = z.number({ description: 'Next Allowed Time' }).parse(nextAllowedTimeStored)


return check({
router: t.router({
spend: t.procedure
.input(z.obj({ units: z.number().min(1) }))
.mutation(async ({ ctx, input }) => {
const now = Date.now() * 0.001
let cooldown = Math.max(nextAllowedTime - now, 0)
if (cooldown > 0) {
return { cooldown }
}

const prev = nextAllowedTime
nextAllowedTime = Math.max(now, nextAllowedTime);
nextAllowedTime += input.units * 0.1;
if (prev !== nextAllowedTime) {
await state.storage.put('nextAllowedTime', nextAllowedTime)
}

// Return the number of seconds that the client needs to wait.
//
// We provide a "grace" period of 20 seconds, meaning that the client can make 4-5 requests
// in a quick burst before they start being limited.
cooldown = Math.max(0, nextAllowedTime - now - 20)

return {
/** Time to wait for next request in seconds */
cooldown
};
})
})
});
}
)

export function createRateLimiterClient(env: Env) {
let isBeingRateLimited = false
return {
async attemptToSpend(ip: string, attemptToSpendUnits: number): Promise<boolean> {
const rateLimiter = RateLimiter.getClient(() => env.limiters.get(env.limiters.idFromName(ip)))
if (isBeingRateLimited) return false
invariant(ip, "Must have IP given")

const { cooldown } = await rateLimiter.spend.mutate({
units: attemptToSpendUnits
})

if (cooldown > 0) {
isBeingRateLimited = true
setTimeout(() => {
isBeingRateLimited = false
}, cooldown * 1000)
return false
}

return true
}
}
}
import { invariant, z } from "@autoplay/utils";
import type { Env } from "../Env";

import { initTRPCDurableObject } from "../helpers/createTrpcDurableObject";

export const tdo = initTRPCDurableObject<Env>();
export const { t } = tdo;

export const RateLimiter = tdo.object(
async (state, _env, check) => {
// Timestamp at which this IP will next be allowed to send a message. Start in the distant
// past, i.e. the IP can send a message now.
const nextAllowedTimeStored = await state.storage.get('nextAllowedTime') ?? 0
let nextAllowedTime = z.number({ description: 'Next Allowed Time' }).parse(nextAllowedTimeStored)


return check({
router: t.router({
spend: t.procedure
.input(z.obj({ units: z.number().min(1) }))
.mutation(async ({ ctx, input }) => {
const now = Date.now() * 0.001
let cooldown = Math.max(nextAllowedTime - now, 0)
if (cooldown > 0) {
return { cooldown }
}

const prev = nextAllowedTime
nextAllowedTime = Math.max(now, nextAllowedTime);
nextAllowedTime += input.units * 0.1;
if (prev !== nextAllowedTime) {
await state.storage.put('nextAllowedTime', nextAllowedTime)
}

// Return the number of seconds that the client needs to wait.
//
// We provide a "grace" period of 20 seconds, meaning that the client can make 4-5 requests
// in a quick burst before they start being limited.
cooldown = Math.max(0, nextAllowedTime - now - 20)

return {
/** Time to wait for next request in seconds */
cooldown
};
})
})
});
}
)

export function createRateLimiterClient(env: Env) {
let isBeingRateLimited = false
return {
async attemptToSpend(ip: string, attemptToSpendUnits: number): Promise<boolean> {
const rateLimiter = RateLimiter.getClient(() => env.limiters.get(env.limiters.idFromName(ip)))
if (isBeingRateLimited) return false
invariant(ip, "Must have IP given")

const { cooldown } = await rateLimiter.spend.mutate({
units: attemptToSpendUnits
})

if (cooldown > 0) {
isBeingRateLimited = true
setTimeout(() => {
isBeingRateLimited = false
}, cooldown * 1000)
return false
}

return true
}
}
}
And usage for another endpoint:
import { z } from "@autoplay/utils";
import { TRPCError } from "@trpc/server";
import { createRateLimiterClient } from "../objects/RateLimiter";
import { t } from "./trpc";

export const createUser = async (ip: string, env: Env) => {
const rateLimiter = createRateLimiterClient(env)
const success = await rateLimiter.attemptToSpend(ip, 50)

if (!success) throw new TRPCError({
message: 'Rate limit reached',
code: "BAD_REQUEST",
})

return {
user_id: "U" + (Date.now() + 1000 * Math.random() + Math.random()).toString(36)
};
});
import { z } from "@autoplay/utils";
import { TRPCError } from "@trpc/server";
import { createRateLimiterClient } from "../objects/RateLimiter";
import { t } from "./trpc";

export const createUser = async (ip: string, env: Env) => {
const rateLimiter = createRateLimiterClient(env)
const success = await rateLimiter.attemptToSpend(ip, 50)

if (!success) throw new TRPCError({
message: 'Rate limit reached',
code: "BAD_REQUEST",
})

return {
user_id: "U" + (Date.now() + 1000 * Math.random() + Math.random()).toString(36)
};
});
WDYT @koudelkaa?
Unknown User
Unknown User•2y ago
Message Not Public
Sign In & Join Server To View