Repository: igorkamyshev/farfetched (ts)

Issue:
Query.reset not aborted in-flight operation
Query.reset (createQuery with onAbort, or createJsonQuery) not aborted in-flight operation.
[reproduce](https://codesandbox.io/p/sandbox/ff-abort-3ygmxh?layout=%257B%2522sidebarPanel%2522%253A%2522EXPLORER%2522%252C%2522rootPanelGroup%2522%253A%257B%2522direction%2522%253A%2522horizontal%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522id%2522%253A%2522ROOT_LAYOUT%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522clujoag4o00063b6j0wjsvthb%2522%252C%2522sizes%2522%253A%255B100%252C0%255D%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522EDITOR%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522id%2522%253A%2522clujoag4o00023b6jnu5k7o70%2522%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522SHELLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522id%2522%253A%2522clujoag4o00033b6jvhol54yh%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522DEVTOOLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522id%2522%253A%2522clujoag4o00053b6j0kay6qgk%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%252C%2522sizes%2522%253A%255B50%252C50%255D%257D%252C%2522tabbedPanels%2522%253A%257B%2522clujoag4o00023b6jnu5k7o70%2522%253A%257B%2522id%2522%253A%2522clujoag4o00023b6jnu5k7o70%2522%252C%2522tabs%2522%253A%255B%255D%257D%252C%2522clujoag4o00053b6j0kay6qgk%2522%253A%257B%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clujoag4o00043b6jl8h5vgby%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522UNASSIGNED_PORT%2522%252C%2522port%2522%253A0%252C%2522path%2522%253A%2522%252F%2522%257D%255D%252C%2522id%2522%253A%2522clujoag4o00053b6j0kay6qgk%2522%252C%2522activeTabId%2522%253A%2522clujoag4o00043b6jl8h5vgby%2522%257D%252C%2522clujoag4o00033b6jvhol54yh%2522%253A%257B%2522tabs%2522%253A%255B%255D%252C%2522id%2522%253A%2522clujoag4o00033b6jvhol54yh%2522%257D%257D%252C%2522showDevtools%2522%253Atrue%252C%2522showShells%2522%253Afalse%252C%2522showSidebar%2522%253Atrue%252C%2522sidebarPanelSize%2522%253A15%257D)

Pull request description:
Abort all in-flight operations on `.reset`
fixes #461
created #478

Patch:
diff --git a/.changeset/eleven-hounds-allow.md b/.changeset/eleven-hounds-allow.md
new file mode 100644
index 0000000..0af4817
--- /dev/null
+++ b/.changeset/eleven-hounds-allow.md
@@ -0,0 +1,5 @@
+---
+"@farfetched/atomic-router": patch
+---
+
+Fix race in `barrierChain`
diff --git a/.changeset/spotty-tigers-roll.md b/.changeset/spotty-tigers-roll.md
new file mode 100644
index 0000000..b5408a4
--- /dev/null
+++ b/.changeset/spotty-tigers-roll.md
@@ -0,0 +1,5 @@
+---
+"@farfetched/core": patch
+---
+
+Abort all in-flight operations on `.reset`
diff --git a/packages/atomic-router/src/barrier.ts b/packages/atomic-router/src/barrier.ts
index 32ca106..8299830 100644
--- a/packages/atomic-router/src/barrier.ts
+++ b/packages/atomic-router/src/barrier.ts
@@ -23,13 +23,7 @@ export function barrierChain(barrier: Barrier): ChainProtocol<any> {
     },
   });
 
-  sample({
-    clock: beforeOpen,
-    target: barrier.__.touch,
-    /* Use batch:false to increase priority of touch call */
-    batch: false,
-  });
-  sample({ clock: beforeOpen, target: fx });
+  sample({ clock: beforeOpen, target: [barrier.__.touch, fx] });
 
   return {
     beforeOpen,
diff --git a/packages/core/package.json b/packages/core/package.json
index cdeb14c..f045101 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -41,7 +41,7 @@
   "size-limit": [
     {
       "path": "./dist/core.js",
-      "limit": "16.2 kB"
+      "limit": "16.3 kB"
     }
   ]
 }
diff --git a/packages/core/src/concurrency/concurrency.ts b/packages/core/src/concurrency/concurrency.ts
index a40cb5c..07ead18 100644
--- a/packages/core/src/concurrency/concurrency.ts
+++ b/packages/core/src/concurrency/concurrency.ts
@@ -1,5 +1,6 @@
 import {
   type Event,
+  type Store,
   createStore,
   sample,
   attach,
@@ -30,37 +31,13 @@ export function concurrency(
 ) {
   if (op.__.meta.flags.concurrencyFieldUsed) {
     console.error(
-      `Both concurrency-operator and concurrency-field are used  on operation ${op.__.meta.name}.`,
+      `Both concurrency-operator and concurrency-field are used on operation ${op.__.meta.name}.`,
       `Please use only concurrency-operator, because field concurrency-field in createJsonQuery and createJsonMutation is deprecated and will be deleted soon.`
     );
   }
   op.__.meta.flags.concurrencyOperatorUsed = true;
 
-  const $callObjects = createStore<CallObject[]>([], { serialize: 'ignore' });
-  sample({
-    clock: op.__.lowLevelAPI.callObjectCreated,
-    source: $callObjects,
-    fn: (callObjects, callObject) =>
-      callObjects.filter((obj) => obj.status === 'pending').concat(callObject),
-    target: $callObjects,
-  });
-
-  const abortManyFx = createEffect((callObjects: CallObject[]) => {
-    callObjects.forEach((callObject) => callObject.abort());
-  });
-
-  const abortAllFx = attach({
-    source: $callObjects,
-    effect: abortManyFx,
-  });
-
-  sample({
-    clock: abortManyFx.done,
-    source: $callObjects,
-    fn: (callObjects, { params: abortedCallObjects }) =>
-      callObjects.filter((obj) => !abortedCallObjects.includes(obj)),
-    target: $callObjects,
-  });
+  const $callObjects = callObejcts(op);
 
   if (strategy) {
     switch (strategy) {
@@ -119,8 +96,49 @@ export function concurrency(
   }
 
   if (abortAll) {
-    sample({ clock: abortAll, target: abortAllFx });
+    abortAllInFlight(op, { clock: abortAll });
   }
 
   return op;
 }
+
+export function abortAllInFlight(
+  op: RemoteOperation<any, any, any, any>,
+  { clock }: { clock: Event<any> }
+) {
+  sample({ clock, source: callObejcts(op), target: abortManyFx });
+}
+
+function callObejcts(
+  op: RemoteOperation<any, any, any, any>
+): Store<CallObject[]> {
+  if (!op.__.meta.$callObjects) {
+    const $callObjects = createStore<CallObject[]>([], { serialize: 'ignore' });
+
+    sample({
+      clock: op.__.lowLevelAPI.callObjectCreated,
+      source: $callObjects,
+      fn: (callObjects, callObject) =>
+        callObjects
+          .filter((obj) => obj.status === 'pending')
+          .concat(callObject),
+      target: $callObjects,
+    });
+
+    sample({
+      clock: abortManyFx.done,
+      source: $callObjects,
+      fn: (callObjects, { params: abortedCallObjects }) =>
+        callObjects.filter((obj) => !abortedCallObjects.includes(obj)),
+      target: $callObjects,
+    });
+
+    op.__.meta.$callObjects = $callObjects;
+  }
+
+  return op.__.meta.$callObjects!;
+}
+
+const abortManyFx = createEffect((callObjects: CallObject[]) =>
+  callObjects.forEach((callObject) => callObject.abort())
+);
diff --git a/packages/core/src/remote_operation/create_remote_operation.ts b/packages/core/src/remote_operation/create_remote_operation.ts
index e216587..540c763 100644
--- a/packages/core/src/remote_operation/create_remote_operation.ts
+++ b/packages/core/src/remote_operation/create_remote_operation.ts
@@ -29,6 +29,7 @@ import { type RemoteOperation } from './type';
 import { get } from '../libs/lohyphen';
 import { isAbortError } from '../errors/guards';
 import { getCallObjectEvent } from './with_call_object';
+import { abortAllInFlight } from '../concurrency/concurrency';
 
 export function createRemoteOperation<
   Params,
@@ -467,7 +468,7 @@ export function createRemoteOperation<
     target: finished.finally,
   });
 
-  return {
+  const op = {
     start,
     finished,
     started,
@@ -499,6 +500,10 @@ export function createRemoteOperation<
       },
     },
   };
+
+  abortAllInFlight(op, { clock: reset });
+
+  return op;
 }
 
 function createDataSourceHandlers<Params>(dataSources: DataSource<Params>[]) {
diff --git a/packages/core/src/remote_operation/type.ts b/packages/core/src/remote_operation/type.ts
index ea9a599..61ffe36 100644
--- a/packages/core/src/remote_operation/type.ts
+++ b/packages/core/src/remote_operation/type.ts
@@ -12,6 +12,7 @@ import type { CallObject } from './with_call_object';
 interface DefaultMeta {
   name: string;
   flags: Record<string, boolean>;
+  $callObjects?: Store<CallObject[]>;
 }
 
 export interface RemoteOperation<


Test patch:
diff --git a/packages/core/src/remote_operation/__test__/create_remote_operation.test.ts b/packages/core/src/remote_operation/__test__/create_remote_operation.test.ts
index fda01da..d9631f5 100644
--- a/packages/core/src/remote_operation/__test__/create_remote_operation.test.ts
+++ b/packages/core/src/remote_operation/__test__/create_remote_operation.test.ts
@@ -1,5 +1,6 @@
 import { allSettled, createStore, createWatch, fork } from 'effector';
 import { describe, test, expect, vi } from 'vitest';
+import { setTimeout } from 'timers/promises';
 
 import { watchRemoteOperation } from '../../test_utils/watch_query';
 import { unknownContract } from '../../contract/unknown_contract';
@@ -8,7 +9,6 @@ import { createRemoteOperation } from '../create_remote_operation';
 import { isTimeoutError } from '../../errors/guards';
 import { timeoutError } from '../../errors/create_error';
 import { onAbort } from '../on_abort';
-import { setTimeout as wait } from 'timers/promises';
 
 const defaultConfig = {
   name: 'test',
@@ -317,9 +317,9 @@ describe('RemoteOperation.__.lowLevelAPI.callObjectCreated', () => {
       unit: operation.__.lowLevelAPI.callObjectCreated,
       scope,
       fn: ({ abort }) => {
-        setTimeout(() => {
+        setTimeout(10).then(() => {
           abort();
-        }, 10);
+        });
       },
     });
 
@@ -337,7 +337,7 @@ describe('RemoteOperation.__.lowLevelAPI.callObjectCreated', () => {
     });
 
     await allSettled(operation.start, { scope, params: 42 });
-    await new Promise((resolve) => setTimeout(resolve, 20));
+    await setTimeout(20);
 
     expect(operationFinished).toBeCalledTimes(1);
     expect(operationFinished.mock.calls.map(([arg]) => arg))
@@ -444,7 +444,7 @@ describe('RemoteOperation and onAbort callback', () => {
     const handleCancel = vi.fn();
 
     operation.__.executeFx.use(async () => {
-      await wait(0);
+      await setTimeout(0);
 
       onAbort(handleCancel);
 
@@ -539,4 +539,29 @@ describe('RemoteOperation and onAbort callback', () => {
     `);
     expect(handleCancel).toBeCalledTimes(0);
   });
+
+  test('abort in-flight operations in case of .reset call, issue #461', async () => {
+    const operation = createRemoteOperation({
+      ...defaultConfig,
+    });
+
+    const handleCancel = vi.fn();
+
+    operation.__.executeFx.use(async () => {
+      onAbort(handleCancel);
+
+      await setTimeout(10);
+
+      return null;
+    });
+
+    const scope = fork();
+
+    allSettled(operation.start, { scope, params: 42 });
+    allSettled(operation.reset, { scope });
+
+    await allSettled(scope);
+
+    expect(handleCancel).toBeCalledTimes(1);
+  });
 });