-
-
Notifications
You must be signed in to change notification settings - Fork 604
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Realtime streams now powered by electric (#1541)
* Realtime streams now powered by electric, and fix the streaming update duplicate issues by converting the electric Shape materialized view into a ReadableStream of changes * Ensure realtime subscription stops when runs are finished, and add an onComplete handle to use realtime hooks * Fix tests
- Loading branch information
Showing
38 changed files
with
1,021 additions
and
465 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
"@trigger.dev/react-hooks": patch | ||
"@trigger.dev/sdk": patch | ||
--- | ||
|
||
Realtime streams now powered by electric. Also, this change fixes a realtime bug that was causing too many re-renders, even on records that didn't change |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
87 changes: 87 additions & 0 deletions
87
apps/webapp/app/routes/realtime.v2.streams.$runId.$streamId.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import { z } from "zod"; | ||
import { $replica } from "~/db.server"; | ||
import { | ||
createActionApiRoute, | ||
createLoaderApiRoute, | ||
} from "~/services/routeBuilders/apiBuilder.server"; | ||
import { v2RealtimeStreams } from "~/services/realtime/v2StreamsGlobal.server"; | ||
|
||
const ParamsSchema = z.object({ | ||
runId: z.string(), | ||
streamId: z.string(), | ||
}); | ||
|
||
const { action } = createActionApiRoute( | ||
{ | ||
params: ParamsSchema, | ||
}, | ||
async ({ request, params, authentication }) => { | ||
if (!request.body) { | ||
return new Response("No body provided", { status: 400 }); | ||
} | ||
|
||
const run = await $replica.taskRun.findFirst({ | ||
where: { | ||
friendlyId: params.runId, | ||
runtimeEnvironmentId: authentication.environment.id, | ||
}, | ||
include: { | ||
batch: { | ||
select: { | ||
friendlyId: true, | ||
}, | ||
}, | ||
}, | ||
}); | ||
|
||
if (!run) { | ||
return new Response("Run not found", { status: 404 }); | ||
} | ||
|
||
return v2RealtimeStreams.ingestData(request.body, run.id, params.streamId); | ||
} | ||
); | ||
|
||
export { action }; | ||
|
||
export const loader = createLoaderApiRoute( | ||
{ | ||
params: ParamsSchema, | ||
allowJWT: true, | ||
corsStrategy: "all", | ||
findResource: async (params, auth) => { | ||
return $replica.taskRun.findFirst({ | ||
where: { | ||
friendlyId: params.runId, | ||
runtimeEnvironmentId: auth.environment.id, | ||
}, | ||
include: { | ||
batch: { | ||
select: { | ||
friendlyId: true, | ||
}, | ||
}, | ||
}, | ||
}); | ||
}, | ||
authorization: { | ||
action: "read", | ||
resource: (run) => ({ | ||
runs: run.friendlyId, | ||
tags: run.runTags, | ||
batch: run.batch?.friendlyId, | ||
tasks: run.taskIdentifier, | ||
}), | ||
superScopes: ["read:runs", "read:all", "admin"], | ||
}, | ||
}, | ||
async ({ params, request, resource: run, authentication }) => { | ||
return v2RealtimeStreams.streamResponse( | ||
request, | ||
run.id, | ||
params.streamId, | ||
authentication.environment, | ||
request.signal | ||
); | ||
} | ||
); |
85 changes: 85 additions & 0 deletions
85
apps/webapp/app/services/realtime/databaseRealtimeStreams.server.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import { PrismaClient } from "@trigger.dev/database"; | ||
import { AuthenticatedEnvironment } from "../apiAuth.server"; | ||
import { logger } from "../logger.server"; | ||
import { RealtimeClient } from "../realtimeClient.server"; | ||
import { StreamIngestor, StreamResponder } from "./types"; | ||
|
||
export type DatabaseRealtimeStreamsOptions = { | ||
prisma: PrismaClient; | ||
realtimeClient: RealtimeClient; | ||
}; | ||
|
||
// Class implementing both interfaces | ||
export class DatabaseRealtimeStreams implements StreamIngestor, StreamResponder { | ||
constructor(private options: DatabaseRealtimeStreamsOptions) {} | ||
|
||
async streamResponse( | ||
request: Request, | ||
runId: string, | ||
streamId: string, | ||
environment: AuthenticatedEnvironment, | ||
signal: AbortSignal | ||
): Promise<Response> { | ||
return this.options.realtimeClient.streamChunks( | ||
request.url, | ||
environment, | ||
runId, | ||
streamId, | ||
signal, | ||
request.headers.get("x-trigger-electric-version") ?? undefined | ||
); | ||
} | ||
|
||
async ingestData( | ||
stream: ReadableStream<Uint8Array>, | ||
runId: string, | ||
streamId: string | ||
): Promise<Response> { | ||
try { | ||
const textStream = stream.pipeThrough(new TextDecoderStream()); | ||
const reader = textStream.getReader(); | ||
let sequence = 0; | ||
|
||
while (true) { | ||
const { done, value } = await reader.read(); | ||
|
||
if (done) { | ||
break; | ||
} | ||
|
||
logger.debug("[DatabaseRealtimeStreams][ingestData] Reading data", { | ||
streamId, | ||
runId, | ||
value, | ||
}); | ||
|
||
const chunks = value | ||
.split("\n") | ||
.filter((chunk) => chunk) // Remove empty lines | ||
.map((line) => { | ||
return { | ||
sequence: sequence++, | ||
value: line, | ||
}; | ||
}); | ||
|
||
await this.options.prisma.realtimeStreamChunk.createMany({ | ||
data: chunks.map((chunk) => { | ||
return { | ||
runId, | ||
key: streamId, | ||
sequence: chunk.sequence, | ||
value: chunk.value, | ||
}; | ||
}), | ||
}); | ||
} | ||
|
||
return new Response(null, { status: 200 }); | ||
} catch (error) { | ||
logger.error("[DatabaseRealtimeStreams][ingestData] Error in ingestData:", { error }); | ||
|
||
return new Response(null, { status: 500 }); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { AuthenticatedEnvironment } from "../apiAuth.server"; | ||
|
||
// Interface for stream ingestion | ||
export interface StreamIngestor { | ||
ingestData( | ||
stream: ReadableStream<Uint8Array>, | ||
runId: string, | ||
streamId: string | ||
): Promise<Response>; | ||
} | ||
|
||
// Interface for stream response | ||
export interface StreamResponder { | ||
streamResponse( | ||
request: Request, | ||
runId: string, | ||
streamId: string, | ||
environment: AuthenticatedEnvironment, | ||
signal: AbortSignal | ||
): Promise<Response>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 13 additions & 0 deletions
13
apps/webapp/app/services/realtime/v2StreamsGlobal.server.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { prisma } from "~/db.server"; | ||
import { singleton } from "~/utils/singleton"; | ||
import { realtimeClient } from "../realtimeClientGlobal.server"; | ||
import { DatabaseRealtimeStreams } from "./databaseRealtimeStreams.server"; | ||
|
||
function initializeDatabaseRealtimeStreams() { | ||
return new DatabaseRealtimeStreams({ | ||
prisma, | ||
realtimeClient, | ||
}); | ||
} | ||
|
||
export const v2RealtimeStreams = singleton("dbRealtimeStreams", initializeDatabaseRealtimeStreams); |
Oops, something went wrong.