@@ -7,7 +7,7 @@ import { BusEvent } from "@/bus/bus-event"
77import { GlobalBus } from "@/bus/global"
88import { Auth } from "@/auth"
99import { SyncEvent } from "@/sync"
10- import { EventTable } from "@/sync/event.sql"
10+ import { EventSequenceTable , EventTable } from "@/sync/event.sql"
1111import { Flag } from "@/flag/flag"
1212import { Log } from "@/util"
1313import { Filesystem } from "@/util"
@@ -23,8 +23,8 @@ import { SessionTable } from "@/session/session.sql"
2323import { SessionID } from "@/session/schema"
2424import { errorData } from "@/util/error"
2525import { AppRuntime } from "@/effect/app-runtime"
26- import { EventSequenceTable } from "@/sync/event.sql"
2726import { waitEvent } from "./util"
27+ import { WorkspaceContext } from "./workspace-context"
2828
2929export const Info = WorkspaceInfo . meta ( {
3030 ref : "Workspace" ,
@@ -297,22 +297,13 @@ export function list(project: Project.Info) {
297297 db . select ( ) . from ( WorkspaceTable ) . where ( eq ( WorkspaceTable . project_id , project . id ) ) . all ( ) ,
298298 )
299299 const spaces = rows . map ( fromRow ) . sort ( ( a , b ) => a . id . localeCompare ( b . id ) )
300-
301- for ( const space of spaces ) startSync ( space )
302300 return spaces
303301}
304302
305- function lookup ( id : WorkspaceID ) {
303+ export const get = fn ( WorkspaceID . zod , async ( id ) => {
306304 const row = Database . use ( ( db ) => db . select ( ) . from ( WorkspaceTable ) . where ( eq ( WorkspaceTable . id , id ) ) . get ( ) )
307305 if ( ! row ) return
308306 return fromRow ( row )
309- }
310-
311- export const get = fn ( WorkspaceID . zod , async ( id ) => {
312- const space = lookup ( id )
313- if ( ! space ) return
314- startSync ( space )
315- return space
316307} )
317308
318309export const remove = fn ( WorkspaceID . zod , async ( id ) => {
@@ -437,6 +428,70 @@ async function connectSSE(url: URL | string, headers: HeadersInit | undefined, s
437428 return res . body
438429}
439430
431+ async function syncHistory ( space : Info , url : URL | string , headers : HeadersInit | undefined , signal : AbortSignal ) {
432+ const sessionIDs = Database . use ( ( db ) =>
433+ db
434+ . select ( { id : SessionTable . id } )
435+ . from ( SessionTable )
436+ . where ( eq ( SessionTable . workspace_id , space . id ) )
437+ . all ( )
438+ . map ( ( row ) => row . id ) ,
439+ )
440+ const state = sessionIDs . length
441+ ? Object . fromEntries (
442+ Database . use ( ( db ) =>
443+ db . select ( ) . from ( EventSequenceTable ) . where ( inArray ( EventSequenceTable . aggregate_id , sessionIDs ) ) . all ( ) ,
444+ ) . map ( ( row ) => [ row . aggregate_id , row . seq ] ) ,
445+ )
446+ : { }
447+
448+ log . info ( "syncing workspace history" , {
449+ workspaceID : space . id ,
450+ sessions : sessionIDs . length ,
451+ known : Object . keys ( state ) . length ,
452+ } )
453+
454+ const requestHeaders = new Headers ( headers )
455+ requestHeaders . set ( "content-type" , "application/json" )
456+
457+ const res = await fetch ( route ( url , "/sync/history" ) , {
458+ method : "POST" ,
459+ headers : requestHeaders ,
460+ body : JSON . stringify ( state ) ,
461+ signal,
462+ } )
463+
464+ if ( ! res . ok ) {
465+ const body = await res . text ( )
466+ throw new Error ( `Workspace history HTTP failure: ${ res . status } ${ body } ` )
467+ }
468+
469+ const events = await res . json ( )
470+
471+ return WorkspaceContext . provide ( {
472+ workspaceID : space . id ,
473+ fn : ( ) => {
474+ for ( const event of events ) {
475+ SyncEvent . replay (
476+ {
477+ id : event . id ,
478+ aggregateID : event . aggregate_id ,
479+ seq : event . seq ,
480+ type : event . type ,
481+ data : event . data ,
482+ } ,
483+ { publish : true } ,
484+ )
485+ }
486+ } ,
487+ } )
488+
489+ log . info ( "workspace history synced" , {
490+ workspaceID : space . id ,
491+ events : events . length ,
492+ } )
493+ }
494+
440495async function syncWorkspaceLoop ( space : Info , signal : AbortSignal ) {
441496 const adaptor = await getAdaptor ( space . projectID , space . type )
442497 const target = await adaptor . target ( space )
@@ -452,7 +507,9 @@ async function syncWorkspaceLoop(space: Info, signal: AbortSignal) {
452507 let stream
453508 try {
454509 stream = await connectSSE ( target . url , target . headers , signal )
510+ await syncHistory ( space , target . url , target . headers , signal )
455511 } catch ( err ) {
512+ stream = null
456513 setStatus ( space . id , "error" )
457514 log . info ( "failed to connect to global sync" , {
458515 workspace : space . name ,
@@ -469,6 +526,7 @@ async function syncWorkspaceLoop(space: Info, signal: AbortSignal) {
469526 await parseSSE ( stream , signal , ( evt : any ) => {
470527 try {
471528 if ( ! ( "payload" in evt ) ) return
529+ if ( evt . payload . type === "server.heartbeat" ) return
472530
473531 if ( evt . payload . type === "sync" ) {
474532 SyncEvent . replay ( evt . payload . syncEvent as SyncEvent . SerializedEvent )
@@ -536,4 +594,19 @@ function stopSync(id: WorkspaceID) {
536594 connections . delete ( id )
537595}
538596
597+ export function startWorkspaceSyncing ( projectID : ProjectID ) {
598+ const spaces = Database . use ( ( db ) =>
599+ db
600+ . select ( { workspace : WorkspaceTable } )
601+ . from ( WorkspaceTable )
602+ . innerJoin ( SessionTable , eq ( SessionTable . workspace_id , WorkspaceTable . id ) )
603+ . where ( eq ( WorkspaceTable . project_id , projectID ) )
604+ . all ( ) ,
605+ )
606+
607+ for ( const row of new Map ( spaces . map ( ( row ) => [ row . workspace . id , row . workspace ] ) ) . values ( ) ) {
608+ void startSync ( fromRow ( row ) )
609+ }
610+ }
611+
539612export * as Workspace from "./workspace"
0 commit comments