mirror of
https://github.com/primedigitaltech/azon_seeker.git
synced 2026-01-28 18:15:29 +08:00
Refactor storage management and error handling; implement task queue for better task execution control
This commit is contained in:
parent
804c2f60c6
commit
74c4f8bb28
@ -1,10 +1,9 @@
|
||||
import { StorageSerializers } from '@vueuse/core';
|
||||
import { pausableWatch, toValue, tryOnScopeDispose } from '@vueuse/shared';
|
||||
import { debounceFilter, pausableWatch, tryOnScopeDispose } from '@vueuse/shared';
|
||||
import { ref, shallowRef } from 'vue-demi';
|
||||
import { storage } from 'webextension-polyfill';
|
||||
|
||||
import type { StorageLikeAsync, UseStorageAsyncOptions } from '@vueuse/core';
|
||||
import type { MaybeRefOrGetter, RemovableRef } from '@vueuse/shared';
|
||||
import type { RemovableRef, StorageLikeAsync, UseStorageAsyncOptions } from '@vueuse/core';
|
||||
import type { Ref } from 'vue-demi';
|
||||
import type { Storage } from 'webextension-polyfill';
|
||||
|
||||
@ -50,95 +49,79 @@ const storageInterface: StorageLikeAsync = {
|
||||
/**
|
||||
* https://github.com/vueuse/vueuse/blob/658444bf9f8b96118dbd06eba411bb6639e24e88/packages/core/useStorageAsync/index.ts
|
||||
*
|
||||
* @param key
|
||||
* @param initialValue
|
||||
* @param options
|
||||
* A custom hook for managing state with Web Extension storage.
|
||||
* This function allows you to synchronize a reactive state with the Web Extension storage API.
|
||||
*
|
||||
* @param key - The key under which the value is stored in the Web Extension storage.
|
||||
* @param initialValue - The initial value to be used if no value is found in storage.
|
||||
* This can be a reactive reference or a plain value.
|
||||
* @param options - Optional settings for the storage behavior.
|
||||
*
|
||||
* @returns A reactive reference to the stored value. The reference can be
|
||||
* removed from the storage by calling its `remove` method.
|
||||
*
|
||||
* @example
|
||||
* const myValue = useWebExtensionStorage2('myKey', 'defaultValue', {
|
||||
* shallow: true,
|
||||
* listenToStorageChanges: true,
|
||||
* });
|
||||
*
|
||||
* // myValue is now a reactive reference that syncs with the Web Extension storage.
|
||||
*/
|
||||
export function useWebExtensionStorage<T>(
|
||||
key: string,
|
||||
initialValue: MaybeRefOrGetter<T>,
|
||||
options: WebExtensionStorageOptions<T> = {},
|
||||
initialValue: MaybeRef<T>,
|
||||
options: Pick<
|
||||
WebExtensionStorageOptions<T>,
|
||||
'shallow' | 'serializer' | 'listenToStorageChanges' | 'flush' | 'deep' | 'eventFilter'
|
||||
> = {},
|
||||
): RemovableRef<T> {
|
||||
const {
|
||||
shallow = false,
|
||||
listenToStorageChanges = true,
|
||||
flush = 'pre',
|
||||
deep = true,
|
||||
listenToStorageChanges = true,
|
||||
writeDefaults = true,
|
||||
mergeDefaults = false,
|
||||
shallow,
|
||||
eventFilter,
|
||||
onError = (e) => {
|
||||
console.error(e);
|
||||
},
|
||||
eventFilter = debounceFilter(1000),
|
||||
} = options;
|
||||
|
||||
const rawInit: T = toValue(initialValue);
|
||||
const rawInit = unref(initialValue);
|
||||
const type = guessSerializerType(rawInit);
|
||||
const data = (shallow ? shallowRef : ref)(rawInit) as Ref<T>;
|
||||
|
||||
const data = (shallow ? shallowRef : ref)(initialValue) as Ref<T>;
|
||||
const serializer = options.serializer ?? StorageSerializers[type];
|
||||
|
||||
async function read(event?: { key: string; newValue: string | null }) {
|
||||
if (event && event.key !== key) return;
|
||||
|
||||
try {
|
||||
const rawValue = event
|
||||
? event.newValue
|
||||
: await storageInterface.getItem(key);
|
||||
if (rawValue == null) {
|
||||
data.value = rawInit;
|
||||
if (writeDefaults && rawInit !== null)
|
||||
await storageInterface.setItem(key, await serializer.write(rawInit));
|
||||
} else if (mergeDefaults) {
|
||||
const value = (await serializer.read(rawValue)) as T;
|
||||
if (typeof mergeDefaults === 'function')
|
||||
data.value = mergeDefaults(value, rawInit);
|
||||
else if (type === 'object' && !Array.isArray(value))
|
||||
data.value = {
|
||||
...(rawInit as Record<keyof unknown, unknown>),
|
||||
...(value as Record<keyof unknown, unknown>),
|
||||
} as T;
|
||||
else data.value = value;
|
||||
} else {
|
||||
data.value = (await serializer.read(rawValue)) as T;
|
||||
}
|
||||
} catch (error) {
|
||||
onError(error);
|
||||
const pullFromStorage = async () => {
|
||||
const rawItem = await storageInterface.getItem(key);
|
||||
if (rawItem) {
|
||||
const item = serializer.read(rawItem) as T;
|
||||
data.value = item;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void read();
|
||||
|
||||
async function write() {
|
||||
try {
|
||||
await (data.value == null
|
||||
? storageInterface.removeItem(key)
|
||||
: storageInterface.setItem(key, await serializer.write(data.value)));
|
||||
} catch (error) {
|
||||
onError(error);
|
||||
const pushToStorage = async () => {
|
||||
const newVal = toRaw(unref(data));
|
||||
if (newVal === null) {
|
||||
await storageInterface.removeItem(key);
|
||||
} else {
|
||||
const item = await serializer.write(newVal);
|
||||
await storageInterface.setItem(key, item);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const { pause: pauseWatch, resume: resumeWatch } = pausableWatch(
|
||||
data,
|
||||
write,
|
||||
{
|
||||
flush,
|
||||
deep,
|
||||
eventFilter,
|
||||
},
|
||||
);
|
||||
const { pause: pauseWatch, resume: resumeWatch } = pausableWatch(data, pushToStorage, {
|
||||
flush,
|
||||
deep,
|
||||
eventFilter,
|
||||
});
|
||||
|
||||
if (listenToStorageChanges) {
|
||||
const listener = async (changes: Record<string, Storage.StorageChange>) => {
|
||||
if (!(key in changes)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
pauseWatch();
|
||||
for (const [key, change] of Object.entries(changes)) {
|
||||
await read({
|
||||
key,
|
||||
newValue: change.newValue as string | null,
|
||||
});
|
||||
}
|
||||
await pullFromStorage();
|
||||
} finally {
|
||||
resumeWatch();
|
||||
}
|
||||
@ -151,5 +134,7 @@ export function useWebExtensionStorage<T>(
|
||||
});
|
||||
}
|
||||
|
||||
return data as RemovableRef<T>;
|
||||
pullFromStorage(); // Init
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
27
src/logic/error-handler.ts
Normal file
27
src/logic/error-handler.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import Emittery from 'emittery';
|
||||
|
||||
export interface ErrorChannelContainer {
|
||||
channel: Emittery<{ error: { message: string } }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process unknown errors.
|
||||
*/
|
||||
export function withErrorHandling(
|
||||
target: (this: ErrorChannelContainer, ...args: any[]) => Promise<any>,
|
||||
_context: ClassMethodDecoratorContext,
|
||||
): (this: ErrorChannelContainer, ...args: any[]) => Promise<any> {
|
||||
// target 就是当前被装饰的 class 方法
|
||||
const originalMethod = target;
|
||||
// 定义一个新方法
|
||||
const decoratedMethod = async function (this: ErrorChannelContainer, ...args: any[]) {
|
||||
try {
|
||||
return await originalMethod.call(this, ...args); // 调用原有方法
|
||||
} catch (error) {
|
||||
this.channel.emit('error', { message: `发生未知错误:${error}` });
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
// 返回装饰后的方法
|
||||
return decoratedMethod;
|
||||
}
|
||||
@ -2,34 +2,14 @@ import Emittery from 'emittery';
|
||||
import type { AmazonDetailItem, AmazonPageWorker, AmazonPageWorkerEvents } from './types';
|
||||
import type { Tabs } from 'webextension-polyfill';
|
||||
import { exec } from '../execute-script';
|
||||
|
||||
/**
|
||||
* Process unknown errors.
|
||||
*/
|
||||
function withErrorHandling(
|
||||
target: (this: AmazonPageWorker, ...args: any[]) => Promise<any>,
|
||||
_context: ClassMethodDecoratorContext,
|
||||
): (this: AmazonPageWorker, ...args: any[]) => Promise<any> {
|
||||
// target 就是当前被装饰的 class 方法
|
||||
const originalMethod = target;
|
||||
// 定义一个新方法
|
||||
const decoratedMethod = async function (this: AmazonPageWorker, ...args: any[]) {
|
||||
try {
|
||||
return await originalMethod.call(this, ...args); // 调用原有方法
|
||||
} catch (error) {
|
||||
this.channel.emit('error', { message: `发生未知错误:${error}` });
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
// 返回装饰后的方法
|
||||
return decoratedMethod;
|
||||
}
|
||||
import { TaskController, TaskQueue, taskUnit } from '../task-queue';
|
||||
import { withErrorHandling } from '../error-handler';
|
||||
|
||||
/**
|
||||
* AmazonPageWorkerImpl can run on background & sidepanel & popup,
|
||||
* **can't** run on content script!
|
||||
*/
|
||||
class AmazonPageWorkerImpl implements AmazonPageWorker {
|
||||
class AmazonPageWorkerImpl implements AmazonPageWorker, TaskController {
|
||||
//#region Singleton
|
||||
private static _instance: AmazonPageWorker | null = null;
|
||||
public static getInstance() {
|
||||
@ -47,9 +27,9 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
||||
readonly channel = new Emittery<AmazonPageWorkerEvents>();
|
||||
|
||||
/**
|
||||
* The signal to interrupt the current operation.
|
||||
* The Task queue
|
||||
*/
|
||||
private _isCancel = false;
|
||||
readonly taskQueue = new TaskQueue();
|
||||
|
||||
private async getCurrentTab(): Promise<Tabs.Tab> {
|
||||
const tab = await browser.tabs
|
||||
@ -173,6 +153,7 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
||||
}
|
||||
|
||||
@withErrorHandling
|
||||
@taskUnit
|
||||
public async doSearch(keywords: string): Promise<string> {
|
||||
const url = new URL('https://www.amazon.com/s');
|
||||
url.searchParams.append('k', keywords);
|
||||
@ -189,14 +170,11 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
||||
}
|
||||
|
||||
@withErrorHandling
|
||||
@taskUnit
|
||||
public async wanderSearchPage(): Promise<void> {
|
||||
const tab = await this.getCurrentTab();
|
||||
this._isCancel = false;
|
||||
const stop = this.channel.on('error', async (_: unknown): Promise<void> => {
|
||||
this._isCancel = true;
|
||||
});
|
||||
let offset = 0;
|
||||
while (!this._isCancel) {
|
||||
while (true) {
|
||||
const { hasNextPage, data } = await this.wanderSearchSinglePage(tab);
|
||||
const keywords = new URL(tab.url!).searchParams.get('k')!;
|
||||
const objs = data.map((r, i) => ({
|
||||
@ -212,12 +190,12 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this._isCancel = false;
|
||||
this.channel.off('error', stop);
|
||||
return new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
@withErrorHandling
|
||||
@taskUnit
|
||||
public async wanderDetailPage(entry: string): Promise<void> {
|
||||
//#region Initial Meta Info
|
||||
const params = { asin: '', url: '' };
|
||||
@ -244,7 +222,7 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
||||
window.scrollBy(0, ~~(Math.random() * 500) + 500);
|
||||
await new Promise((resolve) => setTimeout(resolve, ~~(Math.random() * 50) + 50));
|
||||
const targetNode = document.querySelector(
|
||||
'#prodDetails:has(td), #detailBulletsWrapper_feature_div:has(li)',
|
||||
'#prodDetails:has(td), #detailBulletsWrapper_feature_div:has(li), .av-page-desktop',
|
||||
);
|
||||
if (targetNode && document.readyState !== 'loading') {
|
||||
targetNode.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
@ -303,19 +281,19 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
||||
});
|
||||
if (rawRankingText) {
|
||||
const info: Pick<AmazonDetailItem, 'category1' | 'category2'> = {};
|
||||
let statement = /#[0-9,]+\sin\s\S[\s\w&\(\)]+/.exec(rawRankingText)?.[0];
|
||||
let statement = /#[0-9,]+\sin\s\S[\s\w',\.&\(\)]+/.exec(rawRankingText)?.[0];
|
||||
if (statement) {
|
||||
const name = /(?<=in\s).+(?=\s\(See)/.exec(statement)?.[0] || null;
|
||||
const rank = Number(/(?<=#)[0-9,]+/.exec(statement)?.[0].replace(',', '')) || null;
|
||||
const rank = Number(/(?<=#)[0-9,]+/.exec(statement)?.[0].replaceAll(',', '')) || null;
|
||||
if (name && rank) {
|
||||
info['category1'] = { name, rank };
|
||||
}
|
||||
rawRankingText = rawRankingText.replace(statement, '');
|
||||
}
|
||||
statement = /#[0-9,]+\sin\s\S[\s\w&\(\)]+/.exec(rawRankingText)?.[0];
|
||||
statement = /#[0-9,]+\sin\s\S[\s\w',\.&\(\)]+/.exec(rawRankingText)?.[0];
|
||||
if (statement) {
|
||||
const name = /(?<=in\s).+/.exec(statement)?.[0].replace(/[\s]+$/, '') || null;
|
||||
const rank = Number(/(?<=#)[0-9,]+/.exec(statement)?.[0].replace(',', '')) || null;
|
||||
const rank = Number(/(?<=#)[0-9,]+/.exec(statement)?.[0].replaceAll(',', '')) || null;
|
||||
if (name && rank) {
|
||||
info['category2'] = { name, rank };
|
||||
}
|
||||
@ -376,7 +354,7 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
||||
}
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
this._isCancel = true;
|
||||
this.taskQueue.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
5
src/logic/page-worker/types.d.ts
vendored
5
src/logic/page-worker/types.d.ts
vendored
@ -1,4 +1,5 @@
|
||||
import type Emittery from 'emittery';
|
||||
import { TaskQueue } from '../task-queue';
|
||||
|
||||
type AmazonSearchItem = {
|
||||
keywords: string;
|
||||
@ -16,7 +17,7 @@ type AmazonDetailItem = {
|
||||
ratingCount?: number;
|
||||
category1?: { name: string; rank: number };
|
||||
category2?: { name: string; rank: number };
|
||||
imageUrls: string[];
|
||||
imageUrls?: string[];
|
||||
};
|
||||
|
||||
type AmazonItem = AmazonSearchItem & Partial<AmazonDetailItem> & { hasDetail: boolean };
|
||||
@ -71,7 +72,7 @@ interface AmazonPageWorker {
|
||||
* Browsing goods detail page and collect target information.
|
||||
* @param entry Product link or Amazon Standard Identification Number.
|
||||
*/
|
||||
wanderDetailPage(entry: string): Promise<void>;
|
||||
wanderDetailPage(entry: string | string[]): Promise<void>;
|
||||
|
||||
/**
|
||||
* Stop the worker.
|
||||
|
||||
@ -5,58 +5,54 @@ export const keywordsList = useWebExtensionStorage<string[]>('keywordsList', [''
|
||||
|
||||
export const asinInputText = useWebExtensionStorage<string>('asinInputText', '');
|
||||
|
||||
export const searchItems = useWebExtensionStorage<AmazonSearchItem[]>('itemList', []);
|
||||
export const searchItems = useWebExtensionStorage<AmazonSearchItem[]>('searchItems', []);
|
||||
|
||||
export const detailItems = useWebExtensionStorage<{ [asin: string]: AmazonDetailItem }>(
|
||||
'detailItems',
|
||||
{},
|
||||
);
|
||||
export const detailItems = useWebExtensionStorage<AmazonDetailItem[]>('detailItems', []);
|
||||
|
||||
export const allItems = computed({
|
||||
get() {
|
||||
const sItems = searchItems.value;
|
||||
const dItems = detailItems.value;
|
||||
const dItems = detailItems.value.reduce<Map<string, AmazonDetailItem>>(
|
||||
(m, c) => (m.set(c.asin, c), m),
|
||||
new Map(),
|
||||
);
|
||||
return sItems.map<AmazonItem>((si) => {
|
||||
const asin = si.asin;
|
||||
return asin in dItems
|
||||
? { ...si, ...dItems[asin], hasDetail: true }
|
||||
: { ...si, hasDetail: false };
|
||||
const dItem = dItems.get(asin);
|
||||
return dItem ? { ...si, ...dItem, hasDetail: true } : { ...si, hasDetail: false };
|
||||
});
|
||||
},
|
||||
set(newValue) {
|
||||
const searchItemProps: (keyof AmazonSearchItem)[] = [
|
||||
'keywords',
|
||||
'asin',
|
||||
'title',
|
||||
'imageSrc',
|
||||
'link',
|
||||
'rank',
|
||||
'createTime',
|
||||
];
|
||||
searchItems.value = newValue.map((row) => {
|
||||
const props: (keyof AmazonSearchItem)[] = [
|
||||
'keywords',
|
||||
'asin',
|
||||
'title',
|
||||
'imageSrc',
|
||||
'link',
|
||||
'rank',
|
||||
'createTime',
|
||||
];
|
||||
const entries: [string, unknown][] = Object.entries(row).filter(([key]) =>
|
||||
props.includes(key as keyof AmazonSearchItem),
|
||||
searchItemProps.includes(key as keyof AmazonSearchItem),
|
||||
);
|
||||
return Object.fromEntries(entries) as AmazonSearchItem;
|
||||
});
|
||||
const detailItemsProps: (keyof AmazonDetailItem)[] = [
|
||||
'asin',
|
||||
'category1',
|
||||
'category2',
|
||||
'imageUrls',
|
||||
'rating',
|
||||
'ratingCount',
|
||||
];
|
||||
detailItems.value = newValue
|
||||
.filter((row) => row.hasDetail)
|
||||
.reduce<Record<string, AmazonDetailItem>>((o, row) => {
|
||||
const { asin } = row;
|
||||
const props: (keyof AmazonDetailItem)[] = [
|
||||
'asin',
|
||||
'category1',
|
||||
'category2',
|
||||
'imageUrls',
|
||||
'rating',
|
||||
'ratingCount',
|
||||
];
|
||||
.map((row) => {
|
||||
const entries: [string, unknown][] = Object.entries(row).filter(([key]) =>
|
||||
props.includes(key as keyof AmazonDetailItem),
|
||||
detailItemsProps.includes(key as keyof AmazonDetailItem),
|
||||
);
|
||||
const item = Object.fromEntries(entries) as AmazonDetailItem;
|
||||
o[asin] = item;
|
||||
return o;
|
||||
}, {});
|
||||
return Object.fromEntries(entries) as AmazonSearchItem;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
181
src/logic/task-queue.ts
Normal file
181
src/logic/task-queue.ts
Normal file
@ -0,0 +1,181 @@
|
||||
import Emittery from 'emittery';
|
||||
|
||||
export type TaskExecutionResult<T = undefined> =
|
||||
| {
|
||||
name: string;
|
||||
status: 'success';
|
||||
result: T;
|
||||
}
|
||||
| {
|
||||
name: string;
|
||||
status: 'failure';
|
||||
message: string;
|
||||
};
|
||||
|
||||
export interface TaskInit<
|
||||
T = undefined,
|
||||
F extends (...args: unknown[]) => Promise<T> = (...args: unknown[]) => Promise<T>,
|
||||
> {
|
||||
func: F;
|
||||
args?: Parameters<F>;
|
||||
callback?: (result: TaskExecutionResult<T>) => Promise<void> | void;
|
||||
}
|
||||
|
||||
export class Task<
|
||||
T = undefined,
|
||||
F extends (...args: unknown[]) => Promise<T> = (...args: unknown[]) => Promise<T>,
|
||||
> {
|
||||
private _name: string;
|
||||
private _func: F;
|
||||
private _args: Parameters<F>;
|
||||
private _status: 'initialization' | 'running' | 'success' | 'failure' = 'initialization';
|
||||
private _result: TaskExecutionResult<T> | null = null;
|
||||
private _callback: ((result: TaskExecutionResult<T>) => Promise<void> | void) | undefined;
|
||||
|
||||
public get name() {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
public get status() {
|
||||
return this._status;
|
||||
}
|
||||
|
||||
public get result() {
|
||||
return this._result;
|
||||
}
|
||||
|
||||
constructor(name: string, init: TaskInit<T, F>) {
|
||||
this._name = name;
|
||||
this._func = init.func;
|
||||
this._args = init.args ?? ([] as unknown as Parameters<F>);
|
||||
this._callback = init.callback;
|
||||
}
|
||||
|
||||
public async execute(): Promise<TaskExecutionResult<T>> {
|
||||
const ret = await new Promise<TaskExecutionResult<T>>((resolve) => {
|
||||
this._status = 'running';
|
||||
const task = this._func(...this._args);
|
||||
task
|
||||
.then((ret) => {
|
||||
this._status = 'success';
|
||||
this._result = {
|
||||
name: this.name,
|
||||
status: 'success',
|
||||
result: ret,
|
||||
};
|
||||
resolve(this._result);
|
||||
})
|
||||
.catch((reason) => {
|
||||
this._status = 'failure';
|
||||
this._result = {
|
||||
name: this.name,
|
||||
status: 'failure',
|
||||
message: `${reason}`,
|
||||
};
|
||||
resolve(this._result);
|
||||
});
|
||||
});
|
||||
this._callback && this._callback(ret);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
export class TaskQueue {
|
||||
private _queue: Task<any>[] = [];
|
||||
private _running = false;
|
||||
private _channel: Emittery<{ interrupt: undefined; start: undefined; stop: undefined }> =
|
||||
new Emittery();
|
||||
|
||||
constructor() {
|
||||
this._channel.on('start', () => {
|
||||
this._running = true;
|
||||
});
|
||||
this._channel.on('stop', () => {
|
||||
this._running = false;
|
||||
});
|
||||
}
|
||||
|
||||
public get running() {
|
||||
return this._running;
|
||||
}
|
||||
|
||||
public add<T>(task: Task<T>) {
|
||||
this._queue.push(task);
|
||||
}
|
||||
|
||||
public async start() {
|
||||
this._channel.emit('start');
|
||||
let stopSignal = false;
|
||||
const unsubscribe = this._channel.on('interrupt', () => {
|
||||
stopSignal = true;
|
||||
});
|
||||
while (this._queue.length > 0 && !stopSignal) {
|
||||
const task = this._queue.shift()!;
|
||||
await task.execute();
|
||||
}
|
||||
unsubscribe();
|
||||
this._channel.emit('stop');
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
if (!this._running) {
|
||||
return;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
this._channel.once('stop').then(resolve);
|
||||
this._channel.emit('interrupt');
|
||||
});
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this._queue.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a controller for managing tasks within a task queue.
|
||||
*/
|
||||
export interface TaskController {
|
||||
/**
|
||||
* The queue that manages the tasks for this controller.
|
||||
*/
|
||||
readonly taskQueue: TaskQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* A decorator function that wraps a method to manage its execution as a task in a task queue.
|
||||
*
|
||||
* This function takes a method and returns a new method that, when called, will create a
|
||||
* `Task` and add it to the `taskQueue` of the `TaskController`. The original method will be
|
||||
* executed asynchronously, and the result will be resolved or rejected based on the task's
|
||||
* outcome.
|
||||
*/
|
||||
export function taskUnit<T>(
|
||||
target: (this: TaskController, ...args: any[]) => Promise<T>,
|
||||
context: ClassMethodDecoratorContext,
|
||||
): (this: TaskController, ...args: any[]) => Promise<T> {
|
||||
// target 就是当前被装饰的 class 方法
|
||||
const originalMethod = target;
|
||||
// 定义一个新方法
|
||||
const decoratedMethod = async function (this: TaskController, ...args: any[]) {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const task = new Task<T, typeof originalMethod>(context.name.toString(), {
|
||||
func: (o, ...a) => originalMethod.call(o, ...a),
|
||||
args: [this, ...args],
|
||||
callback: (r) => {
|
||||
if (r.status === 'success') {
|
||||
resolve(r.result);
|
||||
} else if (r.status === 'failure') {
|
||||
reject(r.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
this.taskQueue.add(task);
|
||||
if (!this.taskQueue.running) {
|
||||
this.taskQueue.start();
|
||||
}
|
||||
});
|
||||
};
|
||||
// 返回装饰后的方法
|
||||
return decoratedMethod;
|
||||
}
|
||||
@ -33,7 +33,7 @@ export async function getManifest() {
|
||||
48: './assets/icon-512.png',
|
||||
128: './assets/icon-512.png',
|
||||
},
|
||||
permissions: ['tabs', 'storage', 'activeTab', 'sidePanel', 'scripting'],
|
||||
permissions: ['tabs', 'storage', 'activeTab', 'sidePanel', 'scripting', 'unlimitedStorage'],
|
||||
host_permissions: ['*://*/*'],
|
||||
content_scripts: [
|
||||
{
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormItemRule, UploadOnChange } from 'naive-ui';
|
||||
import pageWorkerFactory from '~/logic/page-worker';
|
||||
import { AmazonDetailItem } from '~/logic/page-worker/types';
|
||||
import { asinInputText, detailItems } from '~/logic/storage';
|
||||
|
||||
const message = useMessage();
|
||||
@ -44,7 +45,7 @@ worker.channel.on('item-rating-collected', (ev) => {
|
||||
time: new Date().toLocaleString(),
|
||||
content: `评分: ${ev.rating};评价数:${ev.ratingCount}`,
|
||||
});
|
||||
detailItems.value[ev.asin] = { ...detailItems.value[ev.asin], ...ev };
|
||||
createOrUpdateDetailItem(ev);
|
||||
});
|
||||
worker.channel.on('item-category-rank-collected', (ev) => {
|
||||
timelines.value.push({
|
||||
@ -56,16 +57,16 @@ worker.channel.on('item-category-rank-collected', (ev) => {
|
||||
ev.category2 ? `#${ev.category2.rank} in ${ev.category2.name}` : '',
|
||||
].join('\n'),
|
||||
});
|
||||
detailItems.value[ev.asin] = { ...detailItems.value[ev.asin], ...ev };
|
||||
createOrUpdateDetailItem(ev);
|
||||
});
|
||||
worker.channel.on('item-images-collected', (ev) => {
|
||||
timelines.value.push({
|
||||
type: 'success',
|
||||
title: `商品${ev.asin}图像`,
|
||||
time: new Date().toLocaleString(),
|
||||
content: `图片数: ${ev.imageUrls.length}`,
|
||||
content: `图片数: ${ev.imageUrls!.length}`,
|
||||
});
|
||||
detailItems.value[ev.asin] = { ...detailItems.value[ev.asin], ...ev };
|
||||
createOrUpdateDetailItem(ev);
|
||||
});
|
||||
|
||||
const handleImportAsin: UploadOnChange = ({ fileList }) => {
|
||||
@ -133,6 +134,17 @@ const handleFetchInfoFromPage = () => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const createOrUpdateDetailItem = (info: AmazonDetailItem) => {
|
||||
const targetIndex = detailItems.value.findLastIndex((item) => info.asin === item.asin);
|
||||
if (targetIndex > -1) {
|
||||
const origin = detailItems.value[targetIndex];
|
||||
const updatedItem = { ...origin, ...info };
|
||||
detailItems.value.splice(targetIndex, 1, updatedItem);
|
||||
} else {
|
||||
detailItems.value.push(info);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user