Documentation Index Fetch the complete documentation index at: https://mintlify.com/botnadzor/extension/llms.txt
Use this file to discover all available pages before exploring further.
Static lists are a core data structure in Botnadzor, providing:
Bot/spam account lists — VK IDs and domains of known bots
Insertion configurations — DOM modification configs for UI injection
Other reference data — Various lookup tables and metadata
The StaticListsService manages fetching lists from remote servers, caching them in IndexedDB, and combining remote data with local user additions/overrides.
Architecture
Key Features
Streaming ingestion — Lists are fetched as JSONL streams and processed line-by-line
Dual instances — Two IndexedDB tables per list (A/B swap for atomic updates)
Combining modes — Three modes: remoteOnly, localOnly, remoteWithLocalOverrides
Local overrides — Users can add items or override remote items
Pollable summaries — Reactive summary statistics (counts, timestamps)
StaticListsService Implementation
The service is registered in the background entrypoint:
src/entrypoints/background.ts
const staticListsService = new StaticListsService ({
aliasManagerForStaticApi ,
rootConfigService ,
});
registerService ( staticListsServiceKey , staticListsService );
// Populate lists when root config is ready
void populateInitialStaticListsIfNeeded ({
rootConfigService ,
staticListsService ,
});
Constructor
The service initializes IndexedDB and creates pollable state for each list:
src/entrypoints/background/@services/static-lists-service.ts
constructor ({
aliasManagerForStaticApi ,
rootConfigService ,
}: {
aliasManagerForStaticApi: AliasManager ;
rootConfigService : RootConfigService ;
}) {
this . aliasManagerForStaticApi = aliasManagerForStaticApi ;
this . rootConfigService = rootConfigService ;
// Initialize IndexedDB with Dexie
this . db = new Dexie ( "static-lists" );
this . db . version ( 2 ). stores ({
[metadataTableName]: "listId" ,
... Object . fromEntries (
staticListDefinitionEntries . flatMap (([ listId , listDefinition ]) => [
[
generateRemoteTableName ( listId , "a" ),
[ "++" , ... listDefinition . indexes ]. join ( "," ),
],
[
generateRemoteTableName ( listId , "b" ),
[ "++" , ... listDefinition . indexes ]. join ( "," ),
],
[
generateLocalTableName ( listId ),
[ "++" , ... listDefinition . indexes ]. join ( "," ),
],
]),
),
});
// Create pollable state for each list
for ( const listId of staticListIds ) {
pollableListMetadataByListId [ listId ] = new Pollable <
StaticListMetadata | undefined
> ( undefined );
pollableListSummaryByListId [ listId ] = new Pollable <
StaticListSummary | undefined
> ( undefined );
}
// Start syncing metadata with DB
void this . startSyncingMetadataWithDb ();
}
Each list has metadata stored in IndexedDB:
type StaticListMetadata = {
listId : StaticListId ;
remoteActiveInstance : "a" | "b" ;
remoteActive ?: {
startedAt : IsoDateTime ;
summary : unknown ;
updatedAt : IsoDateTime ;
upstreamInfo : {
generatedAt : IsoDateTime ;
url : string ;
};
};
remoteNext ?: {
lockId : string ;
startedAt : IsoDateTime ;
summary : unknown ;
updatedAt : IsoDateTime ;
};
combiningMode : "remoteOnly" | "localOnly" | "remoteWithLocalOverrides" ;
localSummary ?: unknown ;
localUpdatedAt ?: IsoDateTime ;
combinedSummary ?: unknown ;
};
Dual Instance System
Each list has two IndexedDB tables: listId_remote_a and listId_remote_b.
Active instance — Currently serving reads
Next instance — Target for new data during updates
This allows atomic swaps: fetch new data into the inactive table, then switch remoteActiveInstance from a to b (or vice versa).
function generateRemoteTableName (
listId : StaticListId ,
instance : StaticListRemoteInstance ,
) : string {
return ` ${ listId } _remote_ ${ instance } ` ;
}
function pickAnotherInstance (
instance : StaticListRemoteInstance ,
) : StaticListRemoteInstance {
return instance === "a" ? "b" : "a" ;
}
Fetching Lists
Lists are fetched from the remote API as JSONL (JSON Lines) streams:
src/entrypoints/background/@services/static-lists-service.ts
public async populateListIfOutdated (
listId : StaticListId ,
toleranceInMinutes : number | undefined ,
): Promise < PopulateFromUrlIfOutdatedResult > {
const listLogger = this . getListLogger ( listId );
listLogger.info( "Populating from if outdated" );
const startedAt = Date.now();
const lockId = nanoid(8);
const rootConfig = await this.rootConfigService.get();
const upstreamInfo =
rootConfig.remoteSystemLookup.staticApi.listLookup[listId];
try {
const initialMetadata = await this.getListMetadata(listId);
if (
this.isListUpToDate(
initialMetadata,
upstreamInfo.generatedAt,
toleranceInMinutes,
)
) {
listLogger.info( "List is up to date" );
return { success: true , data: "updateNotNeeded" };
}
// Acquire lock
const lockMetadata = await this . waitForAnotherLock ( listId , initialMetadata );
this . setListMetadata ({
... lockMetadata ,
remoteNext: {
lockId ,
startedAt: isoDateTimeSchema . parse ( startedAt ),
summary: structuredClone ( mutableSummary ),
updatedAt: isoDateTimeSchema . parse ( startedAt ),
upstreamInfo ,
},
});
// Fetch JSONL stream
const fetchResult = await fetchFromRemoteSystem ({
aliasManager: this . aliasManagerForStaticApi ,
urlSuffix: `/lists/ ${ listId } .jsonl` ,
});
if ( ! fetchResult . success ) {
return {
success: false ,
error: `Failed to fetch list from static API (reason: ${ fetchResult . reason } )` ,
};
}
// Stream and parse JSONL
const nextInstance = pickAnotherInstance (
initialMetadata . remoteActiveInstance ,
);
const nextTable = this . db . table < unknown >(
generateRemoteTableName ( listId , nextInstance )
);
await nextTable . clear ();
let storedItemCount = 0 ;
const listDefinition = staticListDefinitionLookup [ listId ];
const mutableSummary = listDefinition . createEmptySummary ();
for await ( const line of streamLines ( fetchResult . response . body )) {
const receivedItemResult = listDefinition . receivedItemSchema . safeParse (
JSON . parse ( line ),
);
if ( ! receivedItemResult . success ) {
listLogger . error ( "Invalid item received: {error}" , {
error: receivedItemResult . error . message ,
});
continue ;
}
const itemToStore = listDefinition . mapReceivedToStored (
receivedItemResult . data ,
);
itemsToStore . push ( itemToStore );
listDefinition . mutateSummary ( mutableSummary , itemToStore );
if ( itemsToStore . length >= itemBatchSize ) {
await nextTable . bulkAdd ( itemsToStore );
itemsToStore = [];
storedItemCount += itemsToStore . length ;
}
}
// Swap to new instance
this . setListMetadata ({
... metadataWithoutRemoteNext ,
remoteActiveInstance: nextInstance ,
remoteActive: {
startedAt: isoDateTimeSchema . parse ( startedAt ),
summary: finalSummary ,
updatedAt: isoDateTimeSchema . parse ( startedAt ),
upstreamInfo ,
},
});
listLogger . info (
"Populated {storedItemCount} items in {ms}ms" ,
{ storedItemCount , ms: Date . now () - startedAt },
);
return { success: true , data: "updated" };
} catch ( error ) {
listLogger . error ( "Unexpected error while populating: {error}" , { error });
return { success: false , error: String ( error ) };
}
}
Streaming JSONL
Lists can be large (thousands of entries), so they’re streamed and processed in batches:
src/entrypoints/background/@services/static-lists-service.ts
async function* streamLines (
readableStream : ReadableStream < Uint8Array >,
) : AsyncGenerator < string > {
const decoder = new TextDecoder ();
const reader = readableStream . getReader ();
let { value : chunk , done } = await reader . read ();
let buffer = "" ;
while ( ! done ) {
buffer += decoder . decode ( chunk , { stream: true });
const lines = buffer . split ( " \n " );
buffer = lines . pop () ?? "" ;
for ( const line of lines ) {
yield line ;
}
({ value: chunk , done } = await reader . read ());
}
buffer += decoder . decode ();
if ( buffer ) {
for ( const line of buffer . split ( " \n " )) {
if ( line . length > 0 ) {
yield line ;
}
}
}
}
List Summaries
Each list has a summary — aggregated statistics computed during ingestion:
type StaticListSummary = {
count : number ;
// List-specific fields...
};
Summaries are maintained for:
Remote active — Summary of current remote data
Local — Summary of user additions
Combined — Combined summary (respects combining mode)
Summaries are pollable , so clients can reactively update UI when lists change:
let pollVersion : PollVersion | undefined ;
while ( true ) {
const result = await staticListsService . pollListSummary (
pollVersion ,
"insertions" ,
);
pollVersion = result . version ;
console . log ( "Insertions count:" , result . value . count );
}
Summary Computation
Summaries are computed incrementally during ingestion:
const mutableSummary = listDefinition . createEmptySummary ();
for ( const item of items ) {
listDefinition . mutateSummary ( mutableSummary , item );
}
const finalSummary = structuredClone ( mutableSummary );
Each list definition provides:
createEmptySummary() — Creates initial summary object
mutateSummary(summary, item) — Updates summary with new item
unmutateSummary(summary, item) — Reverses update (for overrides)
Combining Modes
The service supports three combining modes:
remoteOnly
Uses only remote data, ignores local additions/overrides.
if ( combiningMode === "remoteOnly" ) {
return findInTable ( await this . getActiveRemoteTable ( listId ));
}
localOnly
Uses only local data, ignores remote data.
if ( combiningMode === "localOnly" ) {
return findInTable ( this . getLocalTable ( listId ));
}
remoteWithLocalOverrides (default)
Combines remote and local data:
Start with all remote items
Override items matching local items (by first index)
Append pure-local items (not in remote)
// Get remote items
const remoteItems = await remoteTable . toArray ();
const localItems = await localTable . toArray ();
// Build local lookup by first index
const localByKey = new Map ();
for ( const item of localItems ) {
const key = item [ firstIndex ];
localByKey . set ( key , item );
}
// Apply local overrides
const result = remoteItems . map (( remoteItem ) => {
const key = remoteItem [ firstIndex ];
return localByKey . get ( key ) ?? remoteItem ;
});
// Append pure-local items
const remoteKeys = new Set ( remoteItems . map (( item ) => item [ firstIndex ]));
for ( const [ key , item ] of localByKey ) {
if ( ! remoteKeys . has ( key )) {
result . push ( item );
}
}
return result ;
Local Item Management
Users can add, update, or remove local items:
Add Local Item
await staticListsService . addLocalItem ( "bots" , {
vkId: 123456 ,
reason: "Spam account" ,
});
Remove Local Item
await staticListsService . removeLocalItem (
"bots" ,
"vkId" , // index
123456 , // value
);
Put Local Item (add or override)
await staticListsService . putLocalItem ( "bots" , {
vkId: 123456 ,
reason: "Updated reason" ,
});
Summary Recomputation
After local changes, summaries are recomputed:
src/entrypoints/background/@services/static-lists-service.ts
private async recomputeLocalAndCombinedSummary (
listId : StaticListId ,
): Promise < void > {
const listDefinition = staticListDefinitionLookup [ listId ];
const localItems = await this . getLocalTable ( listId ). toArray ();
const mutableLocalSummary = listDefinition . createEmptySummary ();
for ( const item of localItems ) {
listDefinition . mutateSummary ( mutableLocalSummary , item );
}
const metadata = await this . getListMetadata ( listId );
const updatedMetadata = await this . recomputeCombinedSummaryForMetadata (
listId ,
{
... metadata ,
localSummary: structuredClone ( mutableLocalSummary ),
localUpdatedAt: isoDateTimeSchema . parse ( Date . now ()),
},
);
this.setListMetadata(updatedMetadata);
}
Item Origin Tracking
The service tracks the origin of each item:
type StaticListItemOrigin = "remote" | "local" | "localOverride" ;
remote — Item only exists in remote list
local — Item only exists locally (pure addition)
localOverride — Local item overrides a remote item
const origin = await staticListsService . getItemOrigin (
"bots" ,
"vkId" ,
123456 ,
);
if ( origin === "localOverride" ) {
console . log ( "This item overrides a remote entry" );
}
Paginated Access
For large lists, the service provides paginated access:
const { items , totalCount } = await staticListsService . getItemsPage (
"bots" ,
{ offset: 0 , limit: 100 },
);
for ( const { item , origin , valid } of items ) {
console . log ( `Item:` , item , `Origin:` , origin , `Valid:` , valid );
}
console . log ( `Total items: ${ totalCount } ` );
Search by Index
Lists have indexed fields for efficient search:
const { items } = await staticListsService . searchItems (
"bots" ,
{ index: "vkId" , value: 123456 },
);
if ( items . length > 0 ) {
console . log ( "Found bot:" , items [ 0 ]. item );
}
Update Orchestration
The background script orchestrates list updates:
src/entrypoints/background.ts
async function populateInitialStaticListsIfNeeded ({
rootConfigService ,
staticListsService ,
} : {
rootConfigService : RootConfigService ;
staticListsService : StaticListsService ;
}) {
let pollVersion : PollVersion | undefined ;
for (;;) {
const result = await rootConfigService . poll ( pollVersion );
pollVersion = result . version ;
const rootConfig = result . value ;
if ( isEqual ( rootConfig , rootConfigSeed )) {
logger . debug (
"Root config is the same as the seed, skipping static lists population" ,
);
continue ;
}
staticListsService . updateIfNeeded ();
}
}
This ensures lists are updated when:
Extension starts (if outdated)
Root config changes (new upstream list URLs)
Streaming ingestion — Memory usage is constant regardless of list size
Batch writes — Items are written in batches of 1000 for optimal performance
IndexedDB — Provides fast indexed lookups
Indexed queries — O(log n) lookup by indexed fields
Full scans — O(n) for paginated access
Pollable state — Clients only re-fetch when data changes
Storage
Compression — JSONL format is compact
Dual instances — 2x storage for atomic updates
Local additions — Separate table, minimal overhead
Next Steps
Proxy Services Learn how to access StaticListsService from content scripts
Insertion System See how insertion configs are fetched and applied