1
- import { BillingClient , Limits , SetPlanBody , UsageSeriesParams } from "@trigger.dev/platform/v3" ;
2
1
import { Organization , Project } from "@trigger.dev/database" ;
2
+ import {
3
+ BillingClient ,
4
+ Limits ,
5
+ SetPlanBody ,
6
+ UsageSeriesParams ,
7
+ UsageResult ,
8
+ } from "@trigger.dev/platform/v3" ;
9
+ import { createCache , DefaultStatefulContext , Namespace } from "@unkey/cache" ;
10
+ import { MemoryStore } from "@unkey/cache/stores" ;
3
11
import { redirect } from "remix-typedjson" ;
4
12
import { $replica } from "~/db.server" ;
5
13
import { env } from "~/env.server" ;
6
14
import { redirectWithErrorMessage , redirectWithSuccessMessage } from "~/models/message.server" ;
7
15
import { createEnvironment } from "~/models/organization.server" ;
8
16
import { logger } from "~/services/logger.server" ;
9
17
import { newProjectPath , organizationBillingPath } from "~/utils/pathBuilder" ;
18
+ import { singleton } from "~/utils/singleton" ;
19
+ import { RedisCacheStore } from "./unkey/redisCacheStore.server" ;
20
+
21
+ function initializeClient ( ) {
22
+ if ( isCloud ( ) && process . env . BILLING_API_URL && process . env . BILLING_API_KEY ) {
23
+ const client = new BillingClient ( {
24
+ url : process . env . BILLING_API_URL ,
25
+ apiKey : process . env . BILLING_API_KEY ,
26
+ } ) ;
27
+ console . log ( `🤑 Billing client initialized: ${ process . env . BILLING_API_URL } ` ) ;
28
+ return client ;
29
+ } else {
30
+ console . log ( `🤑 Billing client not initialized` ) ;
31
+ }
32
+ }
33
+
34
+ const client = singleton ( "billingClient" , initializeClient ) ;
35
+
36
+ function initializePlatformCache ( ) {
37
+ const ctx = new DefaultStatefulContext ( ) ;
38
+ const memory = new MemoryStore ( { persistentMap : new Map ( ) } ) ;
39
+ const redisCacheStore = new RedisCacheStore ( {
40
+ connection : {
41
+ keyPrefix : `cache:platform:v3:` ,
42
+ port : env . REDIS_PORT ,
43
+ host : env . REDIS_HOST ,
44
+ username : env . REDIS_USERNAME ,
45
+ password : env . REDIS_PASSWORD ,
46
+ enableAutoPipelining : true ,
47
+ ...( env . REDIS_TLS_DISABLED === "true" ? { } : { tls : { } } ) ,
48
+ } ,
49
+ } ) ;
50
+
51
+ // This cache holds the limits fetched from the platform service
52
+ const cache = createCache ( {
53
+ limits : new Namespace < number > ( ctx , {
54
+ stores : [ memory , redisCacheStore ] ,
55
+ fresh : 60_000 * 5 , // 5 minutes
56
+ stale : 60_000 * 10 , // 10 minutes
57
+ } ) ,
58
+ usage : new Namespace < UsageResult > ( ctx , {
59
+ stores : [ memory , redisCacheStore ] ,
60
+ fresh : 60_000 * 5 , // 5 minutes
61
+ stale : 60_000 * 10 , // 10 minutes
62
+ } ) ,
63
+ } ) ;
64
+
65
+ return cache ;
66
+ }
67
+
68
+ const platformCache = singleton ( "platformCache" , initializePlatformCache ) ;
10
69
11
70
export async function getCurrentPlan ( orgId : string ) {
12
- const client = getClient ( ) ;
13
71
if ( ! client ) return undefined ;
72
+
14
73
try {
15
74
const result = await client . currentPlan ( orgId ) ;
16
75
@@ -60,8 +119,8 @@ export async function getCurrentPlan(orgId: string) {
60
119
}
61
120
62
121
export async function getLimits ( orgId : string ) {
63
- const client = getClient ( ) ;
64
122
if ( ! client ) return undefined ;
123
+
65
124
try {
66
125
const result = await client . currentPlan ( orgId ) ;
67
126
if ( ! result . success ) {
@@ -87,9 +146,15 @@ export async function getLimit(orgId: string, limit: keyof Limits, fallback: num
87
146
return fallback ;
88
147
}
89
148
149
+ export async function getCachedLimit ( orgId : string , limit : keyof Limits , fallback : number ) {
150
+ return platformCache . limits . swr ( `${ orgId } :${ limit } ` , async ( ) => {
151
+ return getLimit ( orgId , limit , fallback ) ;
152
+ } ) ;
153
+ }
154
+
90
155
export async function customerPortalUrl ( orgId : string , orgSlug : string ) {
91
- const client = getClient ( ) ;
92
156
if ( ! client ) return undefined ;
157
+
93
158
try {
94
159
return client . createPortalSession ( orgId , {
95
160
returnUrl : `${ env . APP_ORIGIN } ${ organizationBillingPath ( { slug : orgSlug } ) } ` ,
@@ -101,8 +166,8 @@ export async function customerPortalUrl(orgId: string, orgSlug: string) {
101
166
}
102
167
103
168
export async function getPlans ( ) {
104
- const client = getClient ( ) ;
105
169
if ( ! client ) return undefined ;
170
+
106
171
try {
107
172
const result = await client . plans ( ) ;
108
173
if ( ! result . success ) {
@@ -122,7 +187,6 @@ export async function setPlan(
122
187
callerPath : string ,
123
188
plan : SetPlanBody
124
189
) {
125
- const client = getClient ( ) ;
126
190
if ( ! client ) {
127
191
throw redirectWithErrorMessage ( callerPath , request , "Error setting plan" ) ;
128
192
}
@@ -178,8 +242,8 @@ export async function setPlan(
178
242
}
179
243
180
244
export async function getUsage ( organizationId : string , { from, to } : { from : Date ; to : Date } ) {
181
- const client = getClient ( ) ;
182
245
if ( ! client ) return undefined ;
246
+
183
247
try {
184
248
const result = await client . usage ( organizationId , { from, to } ) ;
185
249
if ( ! result . success ) {
@@ -193,9 +257,27 @@ export async function getUsage(organizationId: string, { from, to }: { from: Dat
193
257
}
194
258
}
195
259
260
+ export async function getCachedUsage (
261
+ organizationId : string ,
262
+ { from, to } : { from : Date ; to : Date }
263
+ ) {
264
+ if ( ! client ) return undefined ;
265
+
266
+ const result = await platformCache . usage . swr (
267
+ `${ organizationId } :${ from . toISOString ( ) } :${ to . toISOString ( ) } ` ,
268
+ async ( ) => {
269
+ const usageResponse = await getUsage ( organizationId , { from, to } ) ;
270
+
271
+ return usageResponse ;
272
+ }
273
+ ) ;
274
+
275
+ return result . val ;
276
+ }
277
+
196
278
export async function getUsageSeries ( organizationId : string , params : UsageSeriesParams ) {
197
- const client = getClient ( ) ;
198
279
if ( ! client ) return undefined ;
280
+
199
281
try {
200
282
const result = await client . usageSeries ( organizationId , params ) ;
201
283
if ( ! result . success ) {
@@ -214,8 +296,8 @@ export async function reportInvocationUsage(
214
296
costInCents : number ,
215
297
additionalData ?: Record < string , any >
216
298
) {
217
- const client = getClient ( ) ;
218
299
if ( ! client ) return undefined ;
300
+
219
301
try {
220
302
const result = await client . reportInvocationUsage ( {
221
303
organizationId,
@@ -234,8 +316,8 @@ export async function reportInvocationUsage(
234
316
}
235
317
236
318
export async function reportComputeUsage ( request : Request ) {
237
- const client = getClient ( ) ;
238
319
if ( ! client ) return undefined ;
320
+
239
321
return fetch ( `${ process . env . BILLING_API_URL } /api/v1/usage/ingest/compute` , {
240
322
method : "POST" ,
241
323
headers : request . headers ,
@@ -244,8 +326,8 @@ export async function reportComputeUsage(request: Request) {
244
326
}
245
327
246
328
export async function getEntitlement ( organizationId : string ) {
247
- const client = getClient ( ) ;
248
329
if ( ! client ) return undefined ;
330
+
249
331
try {
250
332
const result = await client . getEntitlement ( organizationId ) ;
251
333
if ( ! result . success ) {
@@ -275,19 +357,6 @@ export async function projectCreated(organization: Organization, project: Projec
275
357
}
276
358
}
277
359
278
- function getClient ( ) {
279
- if ( isCloud ( ) && process . env . BILLING_API_URL && process . env . BILLING_API_KEY ) {
280
- const client = new BillingClient ( {
281
- url : process . env . BILLING_API_URL ,
282
- apiKey : process . env . BILLING_API_KEY ,
283
- } ) ;
284
- console . log ( `Billing client initialized: ${ process . env . BILLING_API_URL } ` ) ;
285
- return client ;
286
- } else {
287
- console . log ( `Billing client not initialized` ) ;
288
- }
289
- }
290
-
291
360
function isCloud ( ) : boolean {
292
361
const acceptableHosts = [
293
362
"https://cloud.trigger.dev" ,
0 commit comments