major refactor

This commit is contained in:
BlueSkiesAndGreenPastures
2025-06-20 09:57:20 -05:00
parent 0a5d9f0452
commit 2daf20874d
4 changed files with 101 additions and 125 deletions
+1 -4
View File
@@ -1,4 +1,3 @@
import {adaptiveFetchAndUpdate, eventCache} from './cache/cache.js'
import {Database, envToCfg, httpLogger, LinkService, readEnv} from './index.js' import {Database, envToCfg, httpLogger, LinkService, readEnv} from './index.js'
async function main() { async function main() {
const env = readEnv() const env = readEnv()
@@ -15,13 +14,11 @@ async function main() {
const link = await LinkService.create(cfg) const link = await LinkService.create(cfg)
if (cfg.service.safelink === 1) { if (cfg.service.safelink === 1) {
eventCache.init(cfg.service) cfg.eventCache.adaptiveFetchAndUpdate()
adaptiveFetchAndUpdate()
} }
await link.start() await link.start()
httpLogger.info('link service is running') httpLogger.info('link service is running')
console.log('running service')
process.on('SIGTERM', async () => { process.on('SIGTERM', async () => {
httpLogger.info('link service is stopping') httpLogger.info('link service is stopping')
await link.destroy() await link.destroy()
+96 -112
View File
@@ -8,11 +8,9 @@ let cacheCursor: string | undefined
export class EventCache { export class EventCache {
private rules = new Map<string, ToolsOzoneSafelinkDefs.Event>() private rules = new Map<string, ToolsOzoneSafelinkDefs.Event>()
private cfg: ServiceConfig | undefined = undefined private cfg: ServiceConfig | undefined = undefined
private pollInterval = 1 * 1000 // start at 1 second
constructor() { constructor(cfg: ServiceConfig) {
this.cfg = undefined
}
async init(cfg: ServiceConfig) {
this.cfg = cfg this.cfg = cfg
} }
@@ -37,14 +35,12 @@ export class EventCache {
) )
if (event.action === ToolsOzoneSafelinkDefs.REMOVERULE) { if (event.action === ToolsOzoneSafelinkDefs.REMOVERULE) {
// If the action is to remove the rule, delete it from the cache
this.insert(domain, event) this.insert(domain, event)
redirectLogger.info( redirectLogger.info(
`[EventCache] Removed rule for domain, adjusted audit log: ${domain}`, `[EventCache] Removed rule for domain, adjusted audit log: ${domain}`,
) )
return return
} }
// if update should happen
if (event.action === ToolsOzoneSafelinkDefs.WHITELIST) { if (event.action === ToolsOzoneSafelinkDefs.WHITELIST) {
this.insert(domain, event) this.insert(domain, event)
redirectLogger.info(`[EventCache] Whitelisted domain: ${domain}`) redirectLogger.info(`[EventCache] Whitelisted domain: ${domain}`)
@@ -75,12 +71,10 @@ export class EventCache {
`[EventCache] smartUpdateUrl called for url: ${event.url}, action: ${event.action}`, `[EventCache] smartUpdateUrl called for url: ${event.url}, action: ${event.action}`,
) )
if (event.action === ToolsOzoneSafelinkDefs.REMOVERULE) { if (event.action === ToolsOzoneSafelinkDefs.REMOVERULE) {
// If the action is to remove the rule, update it from the cache
this.insert(event.url, event) this.insert(event.url, event)
redirectLogger.info(`[EventCache] Removed rule for url: ${event.url}`) redirectLogger.info(`[EventCache] Removed rule for url: ${event.url}`)
return return
} }
// if update should happen
if (event.action === ToolsOzoneSafelinkDefs.WHITELIST) { if (event.action === ToolsOzoneSafelinkDefs.WHITELIST) {
this.insert(event.url, event) this.insert(event.url, event)
redirectLogger.info(`[EventCache] Whitelisted url: ${event.url}`) redirectLogger.info(`[EventCache] Whitelisted url: ${event.url}`)
@@ -98,7 +92,6 @@ export class EventCache {
} }
} }
// Insert or update an event
smartUpdate(event: ToolsOzoneSafelinkDefs.Event) { smartUpdate(event: ToolsOzoneSafelinkDefs.Event) {
if (event.pattern === ToolsOzoneSafelinkDefs.DOMAIN) { if (event.pattern === ToolsOzoneSafelinkDefs.DOMAIN) {
redirectLogger.info( redirectLogger.info(
@@ -124,19 +117,16 @@ export class EventCache {
const domain = parsedUrl.hostname const domain = parsedUrl.hostname
const domainAndPath = domain + parsedUrl.pathname const domainAndPath = domain + parsedUrl.pathname
// 1. Check for a rule by domain only
const byDomain = this.rules.get(domain) const byDomain = this.rules.get(domain)
if (byDomain) { if (byDomain) {
return byDomain return byDomain
} }
// 2. Check for a rule by domain + path
const byDomainAndPath = this.rules.get(domainAndPath) const byDomainAndPath = this.rules.get(domainAndPath)
if (byDomainAndPath) { if (byDomainAndPath) {
return byDomainAndPath return byDomainAndPath
} }
// 3. Check for a rule by full URL
return this.rules.get(url) return this.rules.get(url)
} }
@@ -144,112 +134,106 @@ export class EventCache {
this.rules.delete(event.url) this.rules.delete(event.url)
} }
// Get an event by full URL
get(url: string): ToolsOzoneSafelinkDefs.Event | undefined { get(url: string): ToolsOzoneSafelinkDefs.Event | undefined {
const event = this.rules.get(url) const event = this.rules.get(url)
return event return event
} }
// List all events
list(): ToolsOzoneSafelinkDefs.Event[] { list(): ToolsOzoneSafelinkDefs.Event[] {
return Array.from(this.rules.values()) return Array.from(this.rules.values())
} }
}
// Adaptive polling: slow down if no new events, speed up if updates found
// Adaptive polling: slow down if no new events, speed up if updates found async adaptiveFetchAndUpdate() {
let pollInterval = 1 * 1000 // start at 1 seconds const prevCursor = cacheCursor
const eventConfig = await this.getConfig()
export async function adaptiveFetchAndUpdate() { if (eventConfig === undefined) {
const prevCursor = cacheCursor redirectLogger.info(
const eventConfig = await eventCache.getConfig() `[adaptiveFetchAndUpdate] No Configuration found, skipping fetch.`,
if (eventConfig === undefined) { )
redirectLogger.info( } else {
`[adaptiveFetchAndUpdate] No Configuration found, skipping fetch.`, await this.fetchAndUpdateEvents(eventConfig)
) }
} else { if (cacheCursor === prevCursor) {
await fetchAndUpdateEvents(eventConfig) this.pollInterval = Math.min(this.pollInterval * 2, 10 * 60 * 1000)
} redirectLogger.info(
// If no new events, increase interval (up to 10 minutes), else reset to 5s `[adaptiveFetchAndUpdate] No new events, backing off. Next poll in ${
if (cacheCursor === prevCursor) { this.pollInterval / 1000
pollInterval = Math.min(pollInterval * 2, 10 * 60 * 1000) }s`,
redirectLogger.info( )
`[adaptiveFetchAndUpdate] No new events, backing off. Next poll in ${ } else {
pollInterval / 1000 this.pollInterval = 5 * 1000
}s`, redirectLogger.info(
) `[adaptiveFetchAndUpdate] New events found, resetting poll interval to ${
} else { this.pollInterval / 1000
pollInterval = 5 * 1000 }s`,
redirectLogger.info( )
`[adaptiveFetchAndUpdate] New events found, resetting poll interval to ${ }
pollInterval / 1000 setTimeout(() => this.adaptiveFetchAndUpdate(), this.pollInterval)
}s`, }
)
} // Fetch and update events from the server
setTimeout(adaptiveFetchAndUpdate, pollInterval) async fetchAndUpdateEvents(cfg: ServiceConfig) {
} if (!cfg || !cfg.ozoneUrl || !cfg.ozoneAgentHandle || !cfg.ozoneAgentPass) {
console.error(
// Export a singleton instance '[eventCache:fetchAndUpdateEvents] No active config, skipping actions',
export const eventCache = new EventCache() )
// Start adaptive polling return
// adaptiveFetchAndUpdate() }
redirectLogger.info(
// Function to fetch and update events from the server `[eventCache] Fetching events with cursor: ${cacheCursor}`,
export async function fetchAndUpdateEvents(cfg: ServiceConfig) { )
if (!cfg || !cfg.ozoneUrl || !cfg.ozoneAgentHandle || !cfg.ozoneAgentPass) { const ozoneAgent = new OzoneAgent(cfg)
console.error( const ozoneSession = await ozoneAgent.getSession?.()
'[eventCache:fetchAndUpdateEvents] No active config, skipping actions', if (!ozoneSession) {
) console.error(
return '[eventCache:fetchAndUpdateEvents] No active session found.',
} )
redirectLogger.info( return
`[eventCache] Fetching events with cursor: ${cacheCursor}`, }
) const ozoneDid = ozoneSession.did
// Use current session DID instead of env variable if (!ozoneDid || ozoneDid === 'did:plc:invalid') {
const ozoneAgent = await new OzoneAgent(cfg) console.error(
const ozoneSession = await ozoneAgent.getSession?.() '[eventCache:fetchAndUpdateEvents] Invalid or missing session DID.',
if (!ozoneSession) { )
console.error('[eventCache:fetchAndUpdateEvents] No active session found.') return
return }
} ozoneAgent.agent.setHeader?.('atproto-proxy', `${ozoneDid}#atproto_labeler`)
const ozoneDid = ozoneSession.did
if (!ozoneDid || ozoneDid === 'did:plc:invalid') { const res = await ozoneAgent.agent.tools?.ozone?.safelink?.queryEvents?.({
console.error( cursor: cacheCursor,
'[eventCache:fetchAndUpdateEvents] Invalid or missing session DID.', limit: 100,
) })
return
} if (res?.data.cursor === cacheCursor) {
ozoneAgent.agent.setHeader?.('atproto-proxy', `${ozoneDid}#atproto_labeler`) redirectLogger.info(
'[eventCache:fetchAndUpdateEvents] No new events to update.',
const res = await ozoneAgent.agent.tools?.ozone?.safelink?.queryEvents?.({ )
cursor: cacheCursor, return
limit: 100, // Adjust as needed }
})
redirectLogger.info(
if (res?.data.cursor === cacheCursor) { `[eventCache:fetchAndUpdateEvents] Received response:`,
redirectLogger.info( {
'[eventCache:fetchAndUpdateEvents] No new events to update.', ...res,
) data: {
return ...res?.data,
} rules: Array.isArray(res?.data?.events)
? res.data.events.map(event => JSON.stringify(event))
redirectLogger.info(`[eventCache:fetchAndUpdateEvents] Received response:`, { : res?.data?.events,
...res, },
data: { },
...res?.data, )
rules: Array.isArray(res?.data?.events)
? res.data.events.map(event => JSON.stringify(event)) for (const event of res.data.events) {
: res?.data?.events, this.smartUpdate(event)
}, }
})
cacheCursor = res.data?.cursor
for (const event of res.data.events) {
eventCache.smartUpdate(event) redirectLogger.info(
} '[eventCache:fetchAndUpdateEvents] Current cache contents:',
)
cacheCursor = res.data?.cursor redirectLogger.info(this.list())
}
redirectLogger.info(
'[eventCache:fetchAndUpdateEvents] Current cache contents:',
)
redirectLogger.info(eventCache.list())
} }
+3 -1
View File
@@ -1,7 +1,7 @@
import {envInt, envList, envStr} from '@atproto/common' import {envInt, envList, envStr} from '@atproto/common'
// import { type EventCache, eventCache } from '../cache/cache.js' // import { type EventCache, eventCache } from '../cache/cache.js'
import {type EventCache, eventCache} from './cache/cache.js' import {EventCache} from './cache/cache.js'
export type Config = { export type Config = {
service: ServiceConfig service: ServiceConfig
@@ -96,6 +96,8 @@ export const envToCfg = (env: Environment): Config => {
}, },
} }
const eventCache = new EventCache(serviceCfg)
return { return {
service: serviceCfg, service: serviceCfg,
eventCache: eventCache, eventCache: eventCache,
+1 -8
View File
@@ -81,9 +81,8 @@ describe('link service', async () => {
reason: ToolsOzoneSafelinkDefs.SPAM, reason: ToolsOzoneSafelinkDefs.SPAM,
createdBy: 'did:example:admin', createdBy: 'did:example:admin',
createdAt: now, createdAt: now,
comment: 'BONES has been erroneously blocked by due to an error', comment: 'BONES has been erroneously blocked for the sake of this test',
}) })
// Ensure 'later' is after 'now'
const later = new Date(Date.now() + 1000).toISOString() const later = new Date(Date.now() + 1000).toISOString()
linkService.ctx.cfg.eventCache.smartUpdate({ linkService.ctx.cfg.eventCache.smartUpdate({
$type: 'tools.ozone.safelink.defs#event', $type: 'tools.ozone.safelink.defs#event',
@@ -98,12 +97,6 @@ describe('link service', async () => {
comment: comment:
'BONES has been resurrected to bring good music to the world once again', 'BONES has been resurrected to bring good music to the world once again',
}) })
console.log(
linkService.ctx.cfg.eventCache.smartGet(
'https://www.instagram.com/teamseshbones/?hl=en',
),
)
}) })
after(async () => { after(async () => {
await linkService?.destroy() await linkService?.destroy()