mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
fix(book-requests): tell a plugin indexer when its connection changes
Add an optional forget(indexerId) to the plugin contract and forward the host's existing invalidation to it, so a plugin holding a live session drops it when the credential, base URL or private-address setting changes. A plugin that throws or rejects in forget is logged and never fails the save. Fixes #1325
This commit is contained in:
@@ -3,6 +3,7 @@ import { DEFAULT_INDEXER_CATEGORIES, INDEXER_COLORS } from '@bookorbit/types';
|
||||
|
||||
import type { RequestIndexerManagerRow, RequestIndexerRow } from '../../../db/schema';
|
||||
import { IndexerConfigService } from './indexer-config.service';
|
||||
import { PluginIndexerAdapter } from './plugins/plugin-host';
|
||||
|
||||
function indexerRow(overrides: Partial<RequestIndexerRow> = {}): RequestIndexerRow {
|
||||
return {
|
||||
@@ -493,6 +494,41 @@ describe('IndexerConfigService', () => {
|
||||
expect(adapter.forget).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
/** A plugin's in-memory session otherwise outranks the stored credential until the process restarts. */
|
||||
it('drops a plugin-backed session when its connection details change', async () => {
|
||||
const forget = vi.fn();
|
||||
const adapter = new PluginIndexerAdapter(
|
||||
{
|
||||
apiVersion: 1,
|
||||
type: 'demo-tracker',
|
||||
label: 'Demo Tracker',
|
||||
requiresCredential: true,
|
||||
credentialKind: 'sessionId',
|
||||
mediaKinds: ['ebook'],
|
||||
usesCategories: false,
|
||||
seedsBack: true,
|
||||
search: () => Promise.resolve([]),
|
||||
test: () => Promise.resolve({ success: true }),
|
||||
fetchTorrentFile: () => Promise.resolve(new Uint8Array()),
|
||||
forget,
|
||||
},
|
||||
{} as never,
|
||||
);
|
||||
const { service } = makeService({
|
||||
repo: { findById: vi.fn().mockResolvedValue(indexerRow({ adapterType: 'demo-tracker' })) },
|
||||
registry: { require: vi.fn().mockReturnValue(adapter), find: vi.fn().mockReturnValue(adapter) },
|
||||
});
|
||||
|
||||
await service.update(9, { credential: 'a-new-session' });
|
||||
await service.update(9, { baseUrl: 'http://203.0.113.11:9117' });
|
||||
await service.update(9, { allowPrivateAddress: true });
|
||||
expect(forget.mock.calls).toEqual([[9], [9], [9]]);
|
||||
|
||||
forget.mockClear();
|
||||
await service.update(9, { name: 'renamed', enabled: false });
|
||||
expect(forget).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('falls back to the adapter default categories for a medium left blank', async () => {
|
||||
const { service, repo } = makeService();
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ vi.mock('dns/promises', async (importOriginal) => ({
|
||||
}));
|
||||
|
||||
import { lookup } from 'dns/promises';
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
import type { IndexerCredentialStore } from '../indexer-credential-store';
|
||||
import { IndexerSearchException, type ReleaseQuery, type ResolvedIndexerConfig } from '../indexer-adapter';
|
||||
@@ -616,4 +617,42 @@ describe('PluginIndexerAdapter', () => {
|
||||
expect(credentials.rotate).toHaveBeenNthCalledWith(1, 4, 'session-2', 'session-1');
|
||||
expect(credentials.rotate).toHaveBeenNthCalledWith(2, 4, 'session-3', 'session-2');
|
||||
});
|
||||
|
||||
describe('forget', () => {
|
||||
/** A plugin holding a live session would otherwise keep using it until the process restarts. */
|
||||
it('tells the plugin to drop what it holds for the indexer', () => {
|
||||
const forget = vi.fn();
|
||||
const { adapter } = makeAdapter({ forget });
|
||||
|
||||
adapter.forget(12);
|
||||
|
||||
expect(forget).toHaveBeenCalledWith(12);
|
||||
});
|
||||
|
||||
it('does nothing for a plugin written before the hook', () => {
|
||||
expect(() => makeAdapter().adapter.forget(12)).not.toThrow();
|
||||
expect(() => makeAdapter({ forget: true as never }).adapter.forget(12)).not.toThrow();
|
||||
});
|
||||
|
||||
/** The operator's change is already saved by then, so a broken plugin cannot be allowed to fail it. */
|
||||
it('logs a plugin that throws or rejects instead of failing the save', async () => {
|
||||
const warn = vi.spyOn(Logger.prototype, 'warn').mockImplementation(() => undefined);
|
||||
try {
|
||||
expect(() =>
|
||||
makeAdapter({
|
||||
forget: () => {
|
||||
throw new Error('no session map');
|
||||
},
|
||||
}).adapter.forget(12),
|
||||
).not.toThrow();
|
||||
makeAdapter({ forget: () => Promise.reject(new Error('cache closed')) }).adapter.forget(13);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
expect(warn).toHaveBeenCalledWith(expect.stringMatching(/indexerId=12 .*no session map/));
|
||||
expect(warn).toHaveBeenCalledWith(expect.stringMatching(/indexerId=13 .*cache closed/));
|
||||
} finally {
|
||||
warn.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -152,6 +152,26 @@ export class PluginIndexerAdapter implements IndexerAdapter {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs after the operator's change has already been saved, so nothing a plugin does here may fail
|
||||
* that save: a throw or a rejection is logged and dropped.
|
||||
*/
|
||||
forget(indexerId: number): void {
|
||||
if (typeof this.plugin.forget !== 'function') return;
|
||||
try {
|
||||
void Promise.resolve(this.plugin.forget(indexerId)).catch((error: unknown) => this.warnForgetFailed(indexerId, error));
|
||||
} catch (error) {
|
||||
this.warnForgetFailed(indexerId, error);
|
||||
}
|
||||
}
|
||||
|
||||
private warnForgetFailed(indexerId: number, error: unknown): void {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.logger.warn(
|
||||
`[request_indexer.plugin] [fail] indexerId=${indexerId} type=${this.type} error="${sanitizeLogValue(message)}" - the plugin threw while forgetting its session`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* `deadline` is the caller's, where the caller has one. It is applied here rather than handed to
|
||||
* the plugin because a plugin can only check a signal between requests, and the request that
|
||||
|
||||
Reference in New Issue
Block a user