mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
fix(store): guard abort on request supersession (#313)
This commit is contained in:
@@ -2,6 +2,7 @@ import type { Request, RequestMeta, RequestMode } from './request';
|
||||
import type { Reactive } from './state';
|
||||
import type { ErrorTask, PendingTask, SuccessTask, Task, TaskContext, TaskKey } from './task';
|
||||
|
||||
import { abortable } from '@videojs/utils/events';
|
||||
import { isUndefined } from '@videojs/utils/predicate';
|
||||
|
||||
import { StoreError } from './errors';
|
||||
@@ -184,15 +185,7 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
(this.tasks as TasksRecord<Tasks>)[name as keyof Tasks] = pendingTask;
|
||||
|
||||
try {
|
||||
if (abort.signal.aborted) {
|
||||
throw abort.signal.reason || new StoreError('ABORTED');
|
||||
}
|
||||
|
||||
const result = await handler({ input, signal: abort.signal });
|
||||
|
||||
if (abort.signal.aborted) {
|
||||
throw abort.signal.reason || new StoreError('ABORTED');
|
||||
}
|
||||
const result = await abortable(handler({ input, signal: abort.signal }), abort.signal);
|
||||
|
||||
resolve(result);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
import type { Reactive } from './state';
|
||||
import type { PendingTask, Task, TaskContext } from './task';
|
||||
|
||||
import { abortable } from '@videojs/utils/events';
|
||||
import { isNull } from '@videojs/utils/predicate';
|
||||
|
||||
import { StoreError } from './errors';
|
||||
@@ -242,11 +243,7 @@ export class Store<Target, Slices extends AnySlice<Target>[] = AnySlice<Target>[
|
||||
}
|
||||
|
||||
for (const guard of config.guard) {
|
||||
if (signal.aborted) {
|
||||
throw new StoreError('ABORTED');
|
||||
}
|
||||
|
||||
const result = await guard({ target, signal });
|
||||
const result = await abortable(Promise.resolve(guard({ target, signal })), signal);
|
||||
|
||||
if (!result) {
|
||||
throw new StoreError('REJECTED');
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Race a promise against an abort signal. Rejects immediately if the signal
|
||||
* is already aborted or becomes aborted before the promise settles.
|
||||
*/
|
||||
export function abortable<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
|
||||
if (signal.aborted) {
|
||||
return Promise.reject(signal.reason);
|
||||
}
|
||||
|
||||
let onAbort: () => void;
|
||||
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise<never>((_, reject) => {
|
||||
onAbort = () => reject(signal.reason);
|
||||
signal.addEventListener('abort', onAbort, { once: true });
|
||||
}),
|
||||
]).finally(() => {
|
||||
signal.removeEventListener('abort', onAbort);
|
||||
});
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './abort';
|
||||
export * from './disposer';
|
||||
export * from './event-like';
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { abortable } from '../abort';
|
||||
|
||||
describe('abortable', () => {
|
||||
it('resolves when promise resolves before abort', async () => {
|
||||
const controller = new AbortController();
|
||||
const result = await abortable(Promise.resolve('value'), controller.signal);
|
||||
|
||||
expect(result).toBe('value');
|
||||
});
|
||||
|
||||
it('rejects when promise rejects before abort', async () => {
|
||||
const controller = new AbortController();
|
||||
|
||||
await expect(abortable(Promise.reject(new Error('fail')), controller.signal)).rejects.toThrow('fail');
|
||||
});
|
||||
|
||||
it('rejects immediately if signal already aborted', async () => {
|
||||
const controller = new AbortController();
|
||||
const reason = new Error('aborted');
|
||||
|
||||
controller.abort(reason);
|
||||
|
||||
await expect(abortable(Promise.resolve('value'), controller.signal)).rejects.toBe(reason);
|
||||
});
|
||||
|
||||
it('rejects when signal aborts before promise settles', async () => {
|
||||
const controller = new AbortController();
|
||||
const reason = new Error('aborted');
|
||||
const neverResolves = new Promise(() => {});
|
||||
|
||||
const promise = abortable(neverResolves, controller.signal);
|
||||
|
||||
controller.abort(reason);
|
||||
|
||||
await expect(promise).rejects.toBe(reason);
|
||||
});
|
||||
|
||||
it('cleans up abort listener after promise resolves', async () => {
|
||||
const controller = new AbortController();
|
||||
const removeEventListenerSpy = vi.spyOn(controller.signal, 'removeEventListener');
|
||||
|
||||
await abortable(Promise.resolve('value'), controller.signal);
|
||||
|
||||
expect(removeEventListenerSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('cleans up abort listener after promise rejects', async () => {
|
||||
const controller = new AbortController();
|
||||
const removeEventListenerSpy = vi.spyOn(controller.signal, 'removeEventListener');
|
||||
|
||||
await abortable(Promise.reject(new Error('fail')), controller.signal).catch(() => {});
|
||||
|
||||
expect(removeEventListenerSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('cleans up abort listener after abort', async () => {
|
||||
const controller = new AbortController();
|
||||
const removeEventListenerSpy = vi.spyOn(controller.signal, 'removeEventListener');
|
||||
const neverResolves = new Promise(() => {});
|
||||
|
||||
const promise = abortable(neverResolves, controller.signal);
|
||||
|
||||
controller.abort(new Error('aborted'));
|
||||
|
||||
await promise.catch(() => {});
|
||||
|
||||
expect(removeEventListenerSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user