@@ -16,6 +16,12 @@ import {
1616 safeWriteGitCache ,
1717} from './git_cache_layer.ts' ;
1818import { createWebhookTriggerDispatcher } from './trigger_dispatcher.ts' ;
19+ import {
20+ decideRepositoryCompatibility ,
21+ parseRepositoryFromDiscoveryBody ,
22+ type RelayRepositoryIdentity ,
23+ resolveLocalRepositoryIdentity ,
24+ } from './repository_affinity.ts' ;
1925
2026function parsePositiveInt ( raw : string | undefined , fallback : number ) : number {
2127 const value = Number . parseInt ( raw ?? '' , 10 ) ;
@@ -25,6 +31,17 @@ function parsePositiveInt(raw: string | undefined, fallback: number): number {
2531 return Math . trunc ( value ) ;
2632}
2733
34+ function parseCsvList ( raw : string | undefined ) : string [ ] {
35+ if ( typeof raw !== 'string' || raw . trim ( ) . length === 0 ) return [ ] ;
36+ const dedupe = new Set < string > ( ) ;
37+ for ( const part of raw . split ( ',' ) ) {
38+ const value = part . trim ( ) ;
39+ if ( value . length === 0 ) continue ;
40+ dedupe . add ( value ) ;
41+ }
42+ return [ ...dedupe ] ;
43+ }
44+
2845const SESSION_ID_PATTERN = / ^ [ A - Z a - z 0 - 9 ] { 6 , 16 } $ / ;
2946const NAMED_SESSION_PATTERN = / ^ [ A - Z a - z 0 - 9 ] [ A - Z a - z 0 - 9 . _ - ] { 0 , 38 } \/ [ A - Z a - z 0 - 9 ] [ A - Z a - z 0 - 9 . _ - ] { 0 , 63 } $ / ;
3047
@@ -46,6 +63,16 @@ function generateSessionId(): string {
4663const host = Deno . env . get ( 'HOST' ) ?? '127.0.0.1' ;
4764const port = parsePositiveInt ( Deno . env . get ( 'PORT' ) ?? undefined , 8788 ) ;
4865const runtimeConfig = parseRelayRuntimeConfigFromEnv ( ( key ) => Deno . env . get ( key ) ?? undefined ) ;
66+ const repoFfCommitWindow = parsePositiveInt (
67+ Deno . env . get ( 'RELAY_PEER_REPO_FF_COMMIT_WINDOW' ) ?? undefined ,
68+ 30 ,
69+ ) ;
70+ const localRepositoryIdentity = await resolveLocalRepositoryIdentity ( {
71+ explicitRepoId : Deno . env . get ( 'RELAY_REPO_ID' ) ?? undefined ,
72+ explicitOriginUrl : Deno . env . get ( 'RELAY_REPO_ORIGIN_URL' ) ?? undefined ,
73+ explicitRecentCommits : parseCsvList ( Deno . env . get ( 'RELAY_REPO_RECENT_COMMITS' ) ?? undefined ) ,
74+ commitWindow : repoFfCommitWindow ,
75+ } ) ;
4976const relayCacheStore = createMemoryCacheStore ( {
5077 ttlSec : runtimeConfig . cache . ttlSec ,
5178 maxBytes : runtimeConfig . cache . maxBytes ,
@@ -55,6 +82,10 @@ const service = createMemoryRelayService({
5582 peerRelayUrls : runtimeConfig . peers . urls . length > 0
5683 ? runtimeConfig . peers . urls
5784 : runtimeConfig . relay . peerRelayUrls ,
85+ repositoryId : localRepositoryIdentity . repoId ?? undefined ,
86+ repositoryOwner : localRepositoryIdentity . owner ?? undefined ,
87+ repositoryName : localRepositoryIdentity . name ?? undefined ,
88+ repositoryRecentCommits : localRepositoryIdentity . recentCommits ,
5889 cacheStore : relayCacheStore ,
5990 githubWebhookSecret : runtimeConfig . github . webhookSecret ?? undefined ,
6091} ) ;
@@ -117,6 +148,11 @@ function parseCacheExchangePullBody(body: Record<string, unknown>): {
117148 return { entries, nextCursor } ;
118149}
119150
151+ function parsePeerNodeIdFromDiscoveryBody ( body : Record < string , unknown > ) : string | null {
152+ const raw = typeof body . node_id === 'string' ? body . node_id . trim ( ) : '' ;
153+ return raw . length > 0 ? raw : null ;
154+ }
155+
120156function nowEpochSec ( ) : number {
121157 return Math . floor ( Date . now ( ) / 1000 ) ;
122158}
@@ -173,10 +209,84 @@ async function startCacheSyncWorker(): Promise<void> {
173209 return ;
174210 }
175211
212+ const repositoryScopedSyncEnabled = localRepositoryIdentity . name !== null ;
213+ if ( ! repositoryScopedSyncEnabled ) {
214+ console . warn (
215+ '[bit-relay] repository affinity disabled: set RELAY_REPO_ID or enable git origin detection' ,
216+ ) ;
217+ }
218+
219+ const discoveryCacheTtlMs = Math . max ( 5 , runtimeConfig . peers . syncIntervalSec ) * 1000 ;
220+ const loggedRepositoryMismatchPeers = new Set < string > ( ) ;
221+ const peerDiscoveryCache = new Map <
222+ string ,
223+ { expiresAt : number ; nodeId : string | null ; repository : RelayRepositoryIdentity | null }
224+ > ( ) ;
225+
226+ async function fetchPeerDiscovery (
227+ peer : string ,
228+ ) : Promise < { nodeId : string | null ; repository : RelayRepositoryIdentity | null } > {
229+ const url = new URL ( '/api/v1/cache/exchange/discovery' , peer ) ;
230+ const headers = new Headers ( ) ;
231+ if ( peerSyncAuthToken . length > 0 ) {
232+ headers . set ( 'authorization' , `Bearer ${ peerSyncAuthToken } ` ) ;
233+ }
234+ const response = await fetch ( url . toString ( ) , {
235+ method : 'GET' ,
236+ headers,
237+ } ) ;
238+ if ( response . status !== 200 ) {
239+ const text = await response . text ( ) ;
240+ throw new Error (
241+ `peer discovery failed: peer=${ peer } , status=${ response . status } , body=${ text } ` ,
242+ ) ;
243+ }
244+ const body = await response . json ( ) as Record < string , unknown > ;
245+ return {
246+ nodeId : parsePeerNodeIdFromDiscoveryBody ( body ) ,
247+ repository : parseRepositoryFromDiscoveryBody ( body , repoFfCommitWindow ) ,
248+ } ;
249+ }
250+
251+ async function resolvePeerDiscovery (
252+ peer : string ,
253+ ) : Promise < { nodeId : string | null ; repository : RelayRepositoryIdentity | null } > {
254+ const cached = peerDiscoveryCache . get ( peer ) ;
255+ if ( cached && Date . now ( ) < cached . expiresAt ) {
256+ return { nodeId : cached . nodeId , repository : cached . repository } ;
257+ }
258+ const discovered = await fetchPeerDiscovery ( peer ) ;
259+ peerDiscoveryCache . set ( peer , {
260+ expiresAt : Date . now ( ) + discoveryCacheTtlMs ,
261+ nodeId : discovered . nodeId ,
262+ repository : discovered . repository ,
263+ } ) ;
264+ return discovered ;
265+ }
266+
176267 const worker = createCacheSyncWorker ( {
177268 peers,
178269 limit : 200 ,
179270 async pullFromPeer ( { peer, after, limit } ) {
271+ if ( repositoryScopedSyncEnabled ) {
272+ const peerDiscovery = await resolvePeerDiscovery ( peer ) ;
273+ const decision = decideRepositoryCompatibility (
274+ localRepositoryIdentity ,
275+ peerDiscovery . repository ,
276+ ) ;
277+ if ( ! decision . compatible ) {
278+ if ( ! loggedRepositoryMismatchPeers . has ( peer ) ) {
279+ const peerNode = peerDiscovery . nodeId ?? peer ;
280+ console . warn (
281+ `[bit-relay] cache-sync skipped peer=${ peerNode } reason=${ decision . reason } ` ,
282+ ) ;
283+ loggedRepositoryMismatchPeers . add ( peer ) ;
284+ }
285+ return { entries : [ ] , nextCursor : after } ;
286+ }
287+ loggedRepositoryMismatchPeers . delete ( peer ) ;
288+ }
289+
180290 const url = new URL ( '/api/v1/cache/exchange/pull' , peer ) ;
181291 url . searchParams . set ( 'after' , String ( after ) ) ;
182292 url . searchParams . set ( 'limit' , String ( limit ) ) ;
0 commit comments